diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27fda9e..a8d4c5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' @@ -17,7 +19,7 @@ jobs: timeout-minutes: 15 name: lint runs-on: ${{ github.repository == 'stainless-sdks/stagehand-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 @@ -44,7 +46,7 @@ jobs: contents: read id-token: write runs-on: ${{ github.repository == 'stainless-sdks/stagehand-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 @@ -65,14 +67,18 @@ jobs: run: ./scripts/build - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/stagehand-java' + if: |- + github.repository == 'stainless-sdks/stagehand-java' && + !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Build and upload Maven artifacts - if: github.repository == 'stainless-sdks/stagehand-java' + if: |- + github.repository == 'stainless-sdks/stagehand-java' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} diff --git a/.gitignore b/.gitignore index a65171e..dfd2d4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log .gradle .idea .kotlin diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1bc5713..df3292b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.7.1" + ".": "3.18.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index c61740e..129faad 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-a4e672f457dd99336f4b2a113fd7c7c6c9db0941b38d57cff6e3641549a6c4ed.yml -openapi_spec_hash: eae9c8561e420db8e4d238c1e59617fb -config_hash: 2a565ad6662259a2e90fa5f1f5095525 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-bc309fd00fe0507f4cbe3bc77fa27d0fbffeaa6e71998778da34de42608a67e8.yml +openapi_spec_hash: 1db1af5c1b068bba1d652102f4454668 +config_hash: d6c6f623d03971bdba921650e5eb7e5f diff --git a/CHANGELOG.md b/CHANGELOG.md index 168eb3d..d4c566e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## 3.18.0 (2026-03-25) + +Full Changelog: [v0.7.1...v3.18.0](https://github.com/browserbase/stagehand-java/compare/v0.7.1...v3.18.0) + +### Features + +* [STG-1607] Yield finished SSE event instead of silently dropping it ([72fa653](https://github.com/browserbase/stagehand-java/commit/72fa6531d9996c0653fa610b5b05e8027c3a450b)) +* Add explicit SSE event names for local v3 streaming ([6f7efad](https://github.com/browserbase/stagehand-java/commit/6f7efad3e19a4fdb911b2c478afb02ad2b63f8e9)) +* Add missing cdpHeaders field to v3 server openapi spec ([5a9506d](https://github.com/browserbase/stagehand-java/commit/5a9506d5dfeb1ae172834941aad3bbd9e96f2973)) +* Include LLM headers in ModelConfig ([dbf7424](https://github.com/browserbase/stagehand-java/commit/dbf74248557129e0e82067e7ce44bcbc4813174c)) +* Revert broken finished SSE yield config ([f404196](https://github.com/browserbase/stagehand-java/commit/f4041968aefba0e64380ca1d14ff20e5f93f9494)) +* variables for observe ([b10e2ad](https://github.com/browserbase/stagehand-java/commit/b10e2ad429ebac12464ac332eb32c2d11035c1ff)) + + +### Bug Fixes + +* **client:** allow updating header/query affecting fields in `toBuilder()` ([173adb9](https://github.com/browserbase/stagehand-java/commit/173adb9d14281cf3cb1eccedd850f5da1f95f41f)) +* **client:** incorrect `Retry-After` parsing ([76ea3fc](https://github.com/browserbase/stagehand-java/commit/76ea3fc583bfe9ca7295856db85378b44d765b0f)) + + +### Chores + +* **ci:** skip lint on metadata-only changes ([db9534f](https://github.com/browserbase/stagehand-java/commit/db9534f195b6045ad9679b08243c36d55340569b)) +* **ci:** skip uploading artifacts on stainless-internal branches ([4a1399c](https://github.com/browserbase/stagehand-java/commit/4a1399c7a535d9bf8934e9d142878e57a1537b8d)) +* **internal:** bump ktfmt ([d6758da](https://github.com/browserbase/stagehand-java/commit/d6758da0fe85d048b4f8fcd740be7c37901bb253)) +* **internal:** bump palantir-java-format ([3bf036a](https://github.com/browserbase/stagehand-java/commit/3bf036aed5ba8fb90fe4bf5bc6b62a49f09f7dcc)) +* **internal:** codegen related update ([f94c8c9](https://github.com/browserbase/stagehand-java/commit/f94c8c9a86af4592fba5c42645ccc1099a8ab1fe)) +* **internal:** tweak CI branches ([1d88e38](https://github.com/browserbase/stagehand-java/commit/1d88e3802216d33841d3595d3d9fa024189dfc28)) +* **internal:** update gitignore ([690533c](https://github.com/browserbase/stagehand-java/commit/690533ceee3c5d482a17d67c36a7bceb62a63ec2)) +* **internal:** update retry delay tests ([53bdc6b](https://github.com/browserbase/stagehand-java/commit/53bdc6ba4804aca38996fe28d5eeff412a10d189)) + ## 0.7.1 (2026-03-04) Full Changelog: [v0.7.0...v0.7.1](https://github.com/browserbase/stagehand-java/compare/v0.7.0...v0.7.1) diff --git a/README.md b/README.md index 726fa64..4cb92e4 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ -[![Maven Central](https://img.shields.io/maven-central/v/com.browserbase.api/stagehand-java)](https://central.sonatype.com/artifact/com.browserbase.api/stagehand-java/0.7.1) -[![javadoc](https://javadoc.io/badge2/com.browserbase.api/stagehand-java/0.7.1/javadoc.svg)](https://javadoc.io/doc/com.browserbase.api/stagehand-java/0.7.1) +[![Maven Central](https://img.shields.io/maven-central/v/com.browserbase.api/stagehand-java)](https://central.sonatype.com/artifact/com.browserbase.api/stagehand-java/3.18.0) +[![javadoc](https://javadoc.io/badge2/com.browserbase.api/stagehand-java/3.18.0/javadoc.svg)](https://javadoc.io/doc/com.browserbase.api/stagehand-java/3.18.0) @@ -85,7 +85,7 @@ Most existing browser automation tools either require you to write low-level cod ### Gradle ```kotlin -implementation("com.browserbase.api:stagehand-java:0.7.1") +implementation("com.browserbase.api:stagehand-java:3.18.0") ``` ### Maven @@ -94,7 +94,7 @@ implementation("com.browserbase.api:stagehand-java:0.7.1") com.browserbase.api stagehand-java - 0.7.1 + 3.18.0 ``` diff --git a/build.gradle.kts b/build.gradle.kts index 79fb854..e4d25aa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ repositories { allprojects { group = "com.browserbase.api" - version = "0.7.1" // x-release-please-version + version = "3.18.0" // x-release-please-version } subprojects { diff --git a/buildSrc/src/main/kotlin/stagehand.java.gradle.kts b/buildSrc/src/main/kotlin/stagehand.java.gradle.kts index 70fc33f..8f4f902 100644 --- a/buildSrc/src/main/kotlin/stagehand.java.gradle.kts +++ b/buildSrc/src/main/kotlin/stagehand.java.gradle.kts @@ -45,7 +45,7 @@ tasks.withType().configureEach { val palantir by configurations.creating dependencies { - palantir("com.palantir.javaformat:palantir-java-format:2.73.0") + palantir("com.palantir.javaformat:palantir-java-format:2.89.0") } fun registerPalantir( diff --git a/buildSrc/src/main/kotlin/stagehand.kotlin.gradle.kts b/buildSrc/src/main/kotlin/stagehand.kotlin.gradle.kts index be6cf65..2e59660 100644 --- a/buildSrc/src/main/kotlin/stagehand.kotlin.gradle.kts +++ b/buildSrc/src/main/kotlin/stagehand.kotlin.gradle.kts @@ -40,7 +40,7 @@ tasks.withType().configureEach { val ktfmt by configurations.creating dependencies { - ktfmt("com.facebook:ktfmt:0.56") + ktfmt("com.facebook:ktfmt:0.61") } fun registerKtfmt( diff --git a/scripts/fast-format b/scripts/fast-format index 1b3bc47..35a1dee 100755 --- a/scripts/fast-format +++ b/scripts/fast-format @@ -24,8 +24,8 @@ if [ ! -f "$FILE_LIST" ]; then exit 1 fi -if ! command -v ktfmt-fast-format &> /dev/null; then - echo "Error: ktfmt-fast-format not found" +if ! command -v ktfmt &> /dev/null; then + echo "Error: ktfmt not found" exit 1 fi @@ -36,7 +36,7 @@ echo "==> Done looking for Kotlin files" if [[ -n "$kt_files" ]]; then echo "==> will format Kotlin files" - echo "$kt_files" | tr '\n' '\0' | xargs -0 ktfmt-fast-format --kotlinlang-style "$@" + echo "$kt_files" | tr '\n' '\0' | xargs -0 ktfmt --kotlinlang-style "$@" else echo "No Kotlin files to format -- expected outcome during incremental formatting" fi diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ClientOptions.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ClientOptions.kt index 8b1c7f0..ac6a5ef 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ClientOptions.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ClientOptions.kt @@ -485,23 +485,24 @@ private constructor( headers.put("X-Stainless-Runtime", "JRE") headers.put("X-Stainless-Runtime-Version", getJavaVersion()) headers.put("X-Stainless-Kotlin-Version", KotlinVersion.CURRENT.toString()) + // We replace after all the default headers to allow end-users to overwrite them. + headers.replaceAll(this.headers.build()) + queryParams.replaceAll(this.queryParams.build()) browserbaseApiKey.let { if (!it.isEmpty()) { - headers.put("x-bb-api-key", it) + headers.replace("x-bb-api-key", it) } } browserbaseProjectId.let { if (!it.isEmpty()) { - headers.put("x-bb-project-id", it) + headers.replace("x-bb-project-id", it) } } modelApiKey.let { if (!it.isEmpty()) { - headers.put("x-model-api-key", it) + headers.replace("x-model-api-key", it) } } - headers.replaceAll(this.headers.build()) - queryParams.replaceAll(this.queryParams.build()) return ClientOptions( httpClient, diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/SseHandler.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/SseHandler.kt index f8e870e..d0ddde2 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/SseHandler.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/SseHandler.kt @@ -18,23 +18,11 @@ import com.fasterxml.jackson.module.kotlin.jacksonTypeRef internal fun sseHandler(jsonMapper: JsonMapper): Handler> = streamHandler { response, lines -> val state = SseState(jsonMapper) - var done = false for (line in lines) { - // Stop emitting messages, but iterate through the full stream. - if (done) { - continue - } - val message = state.decode(line) ?: continue - when { - message.data.startsWith("{\"data\":{\"status\":\"finished\"") -> { - // In this case we don't break because we still want to iterate through the full - // stream. - done = true - continue - } - message.data.startsWith("error") -> { + when (message.event) { + "error" -> { throw SseException.builder() .statusCode(response.statusCode()) .headers(response.headers()) @@ -47,10 +35,10 @@ internal fun sseHandler(jsonMapper: JsonMapper): Handler yield(message) } } } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/RetryingHttpClient.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/RetryingHttpClient.kt index ab5bd0b..381b36f 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/RetryingHttpClient.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/RetryingHttpClient.kt @@ -201,7 +201,7 @@ private constructor( ?: headers.values("Retry-After").getOrNull(0)?.let { retryAfter -> retryAfter.toFloatOrNull()?.times(TimeUnit.SECONDS.toNanos(1)) ?: try { - ChronoUnit.MILLIS.between( + ChronoUnit.NANOS.between( OffsetDateTime.now(clock), OffsetDateTime.parse( retryAfter, @@ -214,13 +214,8 @@ private constructor( } } ?.let { retryAfterNanos -> - // If the API asks us to wait a certain amount of time (and it's a reasonable - // amount), just - // do what it says. - val retryAfter = Duration.ofNanos(retryAfterNanos.toLong()) - if (retryAfter in Duration.ofNanos(0)..Duration.ofMinutes(1)) { - return retryAfter - } + // If the API asks us to wait a certain amount of time, do what it says. + return Duration.ofNanos(retryAfterNanos.toLong()) } // Apply exponential backoff, but not more than the max. diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/ModelConfig.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/ModelConfig.kt index 1334949..4e46131 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/ModelConfig.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/ModelConfig.kt @@ -8,6 +8,7 @@ import com.browserbase.api.core.JsonField import com.browserbase.api.core.JsonMissing import com.browserbase.api.core.JsonValue import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.toImmutable import com.browserbase.api.errors.StagehandInvalidDataException import com.fasterxml.jackson.annotation.JsonAnyGetter import com.fasterxml.jackson.annotation.JsonAnySetter @@ -24,6 +25,7 @@ private constructor( private val modelName: JsonField, private val apiKey: JsonField, private val baseUrl: JsonField, + private val headers: JsonField, private val provider: JsonField, private val additionalProperties: MutableMap, ) { @@ -33,8 +35,9 @@ private constructor( @JsonProperty("modelName") @ExcludeMissing modelName: JsonField = JsonMissing.of(), @JsonProperty("apiKey") @ExcludeMissing apiKey: JsonField = JsonMissing.of(), @JsonProperty("baseURL") @ExcludeMissing baseUrl: JsonField = JsonMissing.of(), + @JsonProperty("headers") @ExcludeMissing headers: JsonField = JsonMissing.of(), @JsonProperty("provider") @ExcludeMissing provider: JsonField = JsonMissing.of(), - ) : this(modelName, apiKey, baseUrl, provider, mutableMapOf()) + ) : this(modelName, apiKey, baseUrl, headers, provider, mutableMapOf()) /** * Model name string with provider prefix (e.g., 'openai/gpt-5-nano') @@ -60,6 +63,14 @@ private constructor( */ fun baseUrl(): Optional = baseUrl.getOptional("baseURL") + /** + * Custom headers sent with every request to the model provider + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun headers(): Optional = headers.getOptional("headers") + /** * AI provider for the model (or provide a baseURL endpoint instead) * @@ -89,6 +100,13 @@ private constructor( */ @JsonProperty("baseURL") @ExcludeMissing fun _baseUrl(): JsonField = baseUrl + /** + * Returns the raw JSON value of [headers]. + * + * Unlike [headers], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("headers") @ExcludeMissing fun _headers(): JsonField = headers + /** * Returns the raw JSON value of [provider]. * @@ -127,6 +145,7 @@ private constructor( private var modelName: JsonField? = null private var apiKey: JsonField = JsonMissing.of() private var baseUrl: JsonField = JsonMissing.of() + private var headers: JsonField = JsonMissing.of() private var provider: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @@ -135,6 +154,7 @@ private constructor( modelName = modelConfig.modelName apiKey = modelConfig.apiKey baseUrl = modelConfig.baseUrl + headers = modelConfig.headers provider = modelConfig.provider additionalProperties = modelConfig.additionalProperties.toMutableMap() } @@ -173,6 +193,17 @@ private constructor( */ fun baseUrl(baseUrl: JsonField) = apply { this.baseUrl = baseUrl } + /** Custom headers sent with every request to the model provider */ + fun headers(headers: Headers) = headers(JsonField.of(headers)) + + /** + * Sets [Builder.headers] to an arbitrary JSON value. + * + * You should usually call [Builder.headers] with a well-typed [Headers] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun headers(headers: JsonField) = apply { this.headers = headers } + /** AI provider for the model (or provide a baseURL endpoint instead) */ fun provider(provider: Provider) = provider(JsonField.of(provider)) @@ -221,6 +252,7 @@ private constructor( checkRequired("modelName", modelName), apiKey, baseUrl, + headers, provider, additionalProperties.toMutableMap(), ) @@ -236,6 +268,7 @@ private constructor( modelName() apiKey() baseUrl() + headers().ifPresent { it.validate() } provider().ifPresent { it.validate() } validated = true } @@ -258,8 +291,109 @@ private constructor( (if (modelName.asKnown().isPresent) 1 else 0) + (if (apiKey.asKnown().isPresent) 1 else 0) + (if (baseUrl.asKnown().isPresent) 1 else 0) + + (headers.asKnown().getOrNull()?.validity() ?: 0) + (provider.asKnown().getOrNull()?.validity() ?: 0) + /** Custom headers sent with every request to the model provider */ + class Headers + @JsonCreator + private constructor( + @com.fasterxml.jackson.annotation.JsonValue + private val additionalProperties: Map + ) { + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = additionalProperties + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [Headers]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Headers]. */ + class Builder internal constructor() { + + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(headers: Headers) = apply { + additionalProperties = headers.additionalProperties.toMutableMap() + } + + fun additionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.clear() + putAllAdditionalProperties(additionalProperties) + } + + fun putAdditionalProperty(key: String, value: JsonValue) = apply { + additionalProperties.put(key, value) + } + + fun putAllAdditionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.putAll(additionalProperties) + } + + fun removeAdditionalProperty(key: String) = apply { additionalProperties.remove(key) } + + fun removeAllAdditionalProperties(keys: Set) = apply { + keys.forEach(::removeAdditionalProperty) + } + + /** + * Returns an immutable instance of [Headers]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Headers = Headers(additionalProperties.toImmutable()) + } + + private var validated: Boolean = false + + fun validate(): Headers = apply { + if (validated) { + return@apply + } + + validated = true + } + + fun isValid(): Boolean = + try { + validate() + true + } catch (e: StagehandInvalidDataException) { + false + } + + /** + * Returns a score indicating how many valid values are contained in this object + * recursively. + * + * Used for best match union deserialization. + */ + @JvmSynthetic + internal fun validity(): Int = + additionalProperties.count { (_, value) -> !value.isNull() && !value.isMissing() } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Headers && additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = "Headers{additionalProperties=$additionalProperties}" + } + /** AI provider for the model (or provide a baseURL endpoint instead) */ class Provider @JsonCreator private constructor(private val value: JsonField) : Enum { @@ -415,16 +549,17 @@ private constructor( modelName == other.modelName && apiKey == other.apiKey && baseUrl == other.baseUrl && + headers == other.headers && provider == other.provider && additionalProperties == other.additionalProperties } private val hashCode: Int by lazy { - Objects.hash(modelName, apiKey, baseUrl, provider, additionalProperties) + Objects.hash(modelName, apiKey, baseUrl, headers, provider, additionalProperties) } override fun hashCode(): Int = hashCode override fun toString() = - "ModelConfig{modelName=$modelName, apiKey=$apiKey, baseUrl=$baseUrl, provider=$provider, additionalProperties=$additionalProperties}" + "ModelConfig{modelName=$modelName, apiKey=$apiKey, baseUrl=$baseUrl, headers=$headers, provider=$provider, additionalProperties=$additionalProperties}" } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionActParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionActParams.kt index 3445d78..6d92b39 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionActParams.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionActParams.kt @@ -805,7 +805,8 @@ private constructor( fun timeout(): Optional = timeout.getOptional("timeout") /** - * Variables to substitute in the action instruction + * Variables to substitute in the action instruction. Accepts flat primitives or { value, + * description? } objects. * * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if * the server responded with an unexpected value). @@ -899,7 +900,10 @@ private constructor( */ fun timeout(timeout: JsonField) = apply { this.timeout = timeout } - /** Variables to substitute in the action instruction */ + /** + * Variables to substitute in the action instruction. Accepts flat primitives or { + * value, description? } objects. + */ fun variables(variables: Variables) = variables(JsonField.of(variables)) /** @@ -1144,7 +1148,10 @@ private constructor( } } - /** Variables to substitute in the action instruction */ + /** + * Variables to substitute in the action instruction. Accepts flat primitives or { value, + * description? } objects. + */ class Variables @JsonCreator private constructor( diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteParams.kt index 65fd5a6..f68232a 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteParams.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteParams.kt @@ -1686,6 +1686,8 @@ private constructor( private val instruction: JsonField, private val highlightCursor: JsonField, private val maxSteps: JsonField, + private val toolTimeout: JsonField, + private val useSearch: JsonField, private val additionalProperties: MutableMap, ) { @@ -1697,8 +1699,16 @@ private constructor( @JsonProperty("highlightCursor") @ExcludeMissing highlightCursor: JsonField = JsonMissing.of(), - @JsonProperty("maxSteps") @ExcludeMissing maxSteps: JsonField = JsonMissing.of(), - ) : this(instruction, highlightCursor, maxSteps, mutableMapOf()) + @JsonProperty("maxSteps") + @ExcludeMissing + maxSteps: JsonField = JsonMissing.of(), + @JsonProperty("toolTimeout") + @ExcludeMissing + toolTimeout: JsonField = JsonMissing.of(), + @JsonProperty("useSearch") + @ExcludeMissing + useSearch: JsonField = JsonMissing.of(), + ) : this(instruction, highlightCursor, maxSteps, toolTimeout, useSearch, mutableMapOf()) /** * Natural language instruction for the agent @@ -1724,6 +1734,22 @@ private constructor( */ fun maxSteps(): Optional = maxSteps.getOptional("maxSteps") + /** + * Timeout in milliseconds for each agent tool call + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun toolTimeout(): Optional = toolTimeout.getOptional("toolTimeout") + + /** + * Whether to enable the web search tool powered by Browserbase Search API + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun useSearch(): Optional = useSearch.getOptional("useSearch") + /** * Returns the raw JSON value of [instruction]. * @@ -1750,6 +1776,22 @@ private constructor( */ @JsonProperty("maxSteps") @ExcludeMissing fun _maxSteps(): JsonField = maxSteps + /** + * Returns the raw JSON value of [toolTimeout]. + * + * Unlike [toolTimeout], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("toolTimeout") + @ExcludeMissing + fun _toolTimeout(): JsonField = toolTimeout + + /** + * Returns the raw JSON value of [useSearch]. + * + * Unlike [useSearch], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("useSearch") @ExcludeMissing fun _useSearch(): JsonField = useSearch + @JsonAnySetter private fun putAdditionalProperty(key: String, value: JsonValue) { additionalProperties.put(key, value) @@ -1781,6 +1823,8 @@ private constructor( private var instruction: JsonField? = null private var highlightCursor: JsonField = JsonMissing.of() private var maxSteps: JsonField = JsonMissing.of() + private var toolTimeout: JsonField = JsonMissing.of() + private var useSearch: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @JvmSynthetic @@ -1788,6 +1832,8 @@ private constructor( instruction = executeOptions.instruction highlightCursor = executeOptions.highlightCursor maxSteps = executeOptions.maxSteps + toolTimeout = executeOptions.toolTimeout + useSearch = executeOptions.useSearch additionalProperties = executeOptions.additionalProperties.toMutableMap() } @@ -1832,6 +1878,32 @@ private constructor( */ fun maxSteps(maxSteps: JsonField) = apply { this.maxSteps = maxSteps } + /** Timeout in milliseconds for each agent tool call */ + fun toolTimeout(toolTimeout: Double) = toolTimeout(JsonField.of(toolTimeout)) + + /** + * Sets [Builder.toolTimeout] to an arbitrary JSON value. + * + * You should usually call [Builder.toolTimeout] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun toolTimeout(toolTimeout: JsonField) = apply { + this.toolTimeout = toolTimeout + } + + /** Whether to enable the web search tool powered by Browserbase Search API */ + fun useSearch(useSearch: Boolean) = useSearch(JsonField.of(useSearch)) + + /** + * Sets [Builder.useSearch] to an arbitrary JSON value. + * + * You should usually call [Builder.useSearch] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun useSearch(useSearch: JsonField) = apply { this.useSearch = useSearch } + fun additionalProperties(additionalProperties: Map) = apply { this.additionalProperties.clear() putAllAdditionalProperties(additionalProperties) @@ -1868,6 +1940,8 @@ private constructor( checkRequired("instruction", instruction), highlightCursor, maxSteps, + toolTimeout, + useSearch, additionalProperties.toMutableMap(), ) } @@ -1882,6 +1956,8 @@ private constructor( instruction() highlightCursor() maxSteps() + toolTimeout() + useSearch() validated = true } @@ -1903,7 +1979,9 @@ private constructor( internal fun validity(): Int = (if (instruction.asKnown().isPresent) 1 else 0) + (if (highlightCursor.asKnown().isPresent) 1 else 0) + - (if (maxSteps.asKnown().isPresent) 1 else 0) + (if (maxSteps.asKnown().isPresent) 1 else 0) + + (if (toolTimeout.asKnown().isPresent) 1 else 0) + + (if (useSearch.asKnown().isPresent) 1 else 0) override fun equals(other: Any?): Boolean { if (this === other) { @@ -1914,17 +1992,26 @@ private constructor( instruction == other.instruction && highlightCursor == other.highlightCursor && maxSteps == other.maxSteps && + toolTimeout == other.toolTimeout && + useSearch == other.useSearch && additionalProperties == other.additionalProperties } private val hashCode: Int by lazy { - Objects.hash(instruction, highlightCursor, maxSteps, additionalProperties) + Objects.hash( + instruction, + highlightCursor, + maxSteps, + toolTimeout, + useSearch, + additionalProperties, + ) } override fun hashCode(): Int = hashCode override fun toString() = - "ExecuteOptions{instruction=$instruction, highlightCursor=$highlightCursor, maxSteps=$maxSteps, additionalProperties=$additionalProperties}" + "ExecuteOptions{instruction=$instruction, highlightCursor=$highlightCursor, maxSteps=$maxSteps, toolTimeout=$toolTimeout, useSearch=$useSearch, additionalProperties=$additionalProperties}" } /** Whether to stream the response via SSE */ diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionObserveParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionObserveParams.kt index 695025f..d18eb4a 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionObserveParams.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionObserveParams.kt @@ -14,6 +14,7 @@ import com.browserbase.api.core.allMaxBy import com.browserbase.api.core.getOrThrow import com.browserbase.api.core.http.Headers import com.browserbase.api.core.http.QueryParams +import com.browserbase.api.core.toImmutable import com.browserbase.api.errors.StagehandInvalidDataException import com.fasterxml.jackson.annotation.JsonAnyGetter import com.fasterxml.jackson.annotation.JsonAnySetter @@ -566,6 +567,7 @@ private constructor( private val model: JsonField, private val selector: JsonField, private val timeout: JsonField, + private val variables: JsonField, private val additionalProperties: MutableMap, ) { @@ -576,7 +578,10 @@ private constructor( @ExcludeMissing selector: JsonField = JsonMissing.of(), @JsonProperty("timeout") @ExcludeMissing timeout: JsonField = JsonMissing.of(), - ) : this(model, selector, timeout, mutableMapOf()) + @JsonProperty("variables") + @ExcludeMissing + variables: JsonField = JsonMissing.of(), + ) : this(model, selector, timeout, variables, mutableMapOf()) /** * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') @@ -602,6 +607,16 @@ private constructor( */ fun timeout(): Optional = timeout.getOptional("timeout") + /** + * Variables whose names are exposed to the model so observe() returns %variableName% + * placeholders in suggested action arguments instead of literal values. Accepts flat + * primitives or { value, description? } objects. + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun variables(): Optional = variables.getOptional("variables") + /** * Returns the raw JSON value of [model]. * @@ -623,6 +638,15 @@ private constructor( */ @JsonProperty("timeout") @ExcludeMissing fun _timeout(): JsonField = timeout + /** + * Returns the raw JSON value of [variables]. + * + * Unlike [variables], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("variables") + @ExcludeMissing + fun _variables(): JsonField = variables + @JsonAnySetter private fun putAdditionalProperty(key: String, value: JsonValue) { additionalProperties.put(key, value) @@ -647,6 +671,7 @@ private constructor( private var model: JsonField = JsonMissing.of() private var selector: JsonField = JsonMissing.of() private var timeout: JsonField = JsonMissing.of() + private var variables: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @JvmSynthetic @@ -654,6 +679,7 @@ private constructor( model = options.model selector = options.selector timeout = options.timeout + variables = options.variables additionalProperties = options.additionalProperties.toMutableMap() } @@ -699,6 +725,22 @@ private constructor( */ fun timeout(timeout: JsonField) = apply { this.timeout = timeout } + /** + * Variables whose names are exposed to the model so observe() returns %variableName% + * placeholders in suggested action arguments instead of literal values. Accepts flat + * primitives or { value, description? } objects. + */ + fun variables(variables: Variables) = variables(JsonField.of(variables)) + + /** + * Sets [Builder.variables] to an arbitrary JSON value. + * + * You should usually call [Builder.variables] with a well-typed [Variables] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun variables(variables: JsonField) = apply { this.variables = variables } + fun additionalProperties(additionalProperties: Map) = apply { this.additionalProperties.clear() putAllAdditionalProperties(additionalProperties) @@ -724,7 +766,7 @@ private constructor( * Further updates to this [Builder] will not mutate the returned instance. */ fun build(): Options = - Options(model, selector, timeout, additionalProperties.toMutableMap()) + Options(model, selector, timeout, variables, additionalProperties.toMutableMap()) } private var validated: Boolean = false @@ -737,6 +779,7 @@ private constructor( model().ifPresent { it.validate() } selector() timeout() + variables().ifPresent { it.validate() } validated = true } @@ -758,7 +801,8 @@ private constructor( internal fun validity(): Int = (model.asKnown().getOrNull()?.validity() ?: 0) + (if (selector.asKnown().isPresent) 1 else 0) + - (if (timeout.asKnown().isPresent) 1 else 0) + (if (timeout.asKnown().isPresent) 1 else 0) + + (variables.asKnown().getOrNull()?.validity() ?: 0) /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ @JsonDeserialize(using = Model.Deserializer::class) @@ -932,6 +976,113 @@ private constructor( } } + /** + * Variables whose names are exposed to the model so observe() returns %variableName% + * placeholders in suggested action arguments instead of literal values. Accepts flat + * primitives or { value, description? } objects. + */ + class Variables + @JsonCreator + private constructor( + @com.fasterxml.jackson.annotation.JsonValue + private val additionalProperties: Map + ) { + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = additionalProperties + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [Variables]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Variables]. */ + class Builder internal constructor() { + + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(variables: Variables) = apply { + additionalProperties = variables.additionalProperties.toMutableMap() + } + + fun additionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.clear() + putAllAdditionalProperties(additionalProperties) + } + + fun putAdditionalProperty(key: String, value: JsonValue) = apply { + additionalProperties.put(key, value) + } + + fun putAllAdditionalProperties(additionalProperties: Map) = + apply { + this.additionalProperties.putAll(additionalProperties) + } + + fun removeAdditionalProperty(key: String) = apply { + additionalProperties.remove(key) + } + + fun removeAllAdditionalProperties(keys: Set) = apply { + keys.forEach(::removeAdditionalProperty) + } + + /** + * Returns an immutable instance of [Variables]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Variables = Variables(additionalProperties.toImmutable()) + } + + private var validated: Boolean = false + + fun validate(): Variables = apply { + if (validated) { + return@apply + } + + validated = true + } + + fun isValid(): Boolean = + try { + validate() + true + } catch (e: StagehandInvalidDataException) { + false + } + + /** + * Returns a score indicating how many valid values are contained in this object + * recursively. + * + * Used for best match union deserialization. + */ + @JvmSynthetic + internal fun validity(): Int = + additionalProperties.count { (_, value) -> !value.isNull() && !value.isMissing() } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Variables && additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = "Variables{additionalProperties=$additionalProperties}" + } + override fun equals(other: Any?): Boolean { if (this === other) { return true @@ -941,17 +1092,18 @@ private constructor( model == other.model && selector == other.selector && timeout == other.timeout && + variables == other.variables && additionalProperties == other.additionalProperties } private val hashCode: Int by lazy { - Objects.hash(model, selector, timeout, additionalProperties) + Objects.hash(model, selector, timeout, variables, additionalProperties) } override fun hashCode(): Int = hashCode override fun toString() = - "Options{model=$model, selector=$selector, timeout=$timeout, additionalProperties=$additionalProperties}" + "Options{model=$model, selector=$selector, timeout=$timeout, variables=$variables, additionalProperties=$additionalProperties}" } /** Whether to stream the response via SSE */ diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionStartParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionStartParams.kt index f177a4e..5e09071 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionStartParams.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionStartParams.kt @@ -1372,6 +1372,7 @@ private constructor( private constructor( private val acceptDownloads: JsonField, private val args: JsonField>, + private val cdpHeaders: JsonField, private val cdpUrl: JsonField, private val chromiumSandbox: JsonField, private val connectTimeoutMs: JsonField, @@ -1400,6 +1401,9 @@ private constructor( @JsonProperty("args") @ExcludeMissing args: JsonField> = JsonMissing.of(), + @JsonProperty("cdpHeaders") + @ExcludeMissing + cdpHeaders: JsonField = JsonMissing.of(), @JsonProperty("cdpUrl") @ExcludeMissing cdpUrl: JsonField = JsonMissing.of(), @@ -1450,6 +1454,7 @@ private constructor( ) : this( acceptDownloads, args, + cdpHeaders, cdpUrl, chromiumSandbox, connectTimeoutMs, @@ -1483,6 +1488,12 @@ private constructor( */ fun args(): Optional> = args.getOptional("args") + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun cdpHeaders(): Optional = cdpHeaders.getOptional("cdpHeaders") + /** * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. * if the server responded with an unexpected value). @@ -1608,6 +1619,16 @@ private constructor( */ @JsonProperty("args") @ExcludeMissing fun _args(): JsonField> = args + /** + * Returns the raw JSON value of [cdpHeaders]. + * + * Unlike [cdpHeaders], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("cdpHeaders") + @ExcludeMissing + fun _cdpHeaders(): JsonField = cdpHeaders + /** * Returns the raw JSON value of [cdpUrl]. * @@ -1783,6 +1804,7 @@ private constructor( private var acceptDownloads: JsonField = JsonMissing.of() private var args: JsonField>? = null + private var cdpHeaders: JsonField = JsonMissing.of() private var cdpUrl: JsonField = JsonMissing.of() private var chromiumSandbox: JsonField = JsonMissing.of() private var connectTimeoutMs: JsonField = JsonMissing.of() @@ -1806,6 +1828,7 @@ private constructor( internal fun from(launchOptions: LaunchOptions) = apply { acceptDownloads = launchOptions.acceptDownloads args = launchOptions.args.map { it.toMutableList() } + cdpHeaders = launchOptions.cdpHeaders cdpUrl = launchOptions.cdpUrl chromiumSandbox = launchOptions.chromiumSandbox connectTimeoutMs = launchOptions.connectTimeoutMs @@ -1865,6 +1888,19 @@ private constructor( } } + fun cdpHeaders(cdpHeaders: CdpHeaders) = cdpHeaders(JsonField.of(cdpHeaders)) + + /** + * Sets [Builder.cdpHeaders] to an arbitrary JSON value. + * + * You should usually call [Builder.cdpHeaders] with a well-typed [CdpHeaders] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun cdpHeaders(cdpHeaders: JsonField) = apply { + this.cdpHeaders = cdpHeaders + } + fun cdpUrl(cdpUrl: String) = cdpUrl(JsonField.of(cdpUrl)) /** @@ -2120,6 +2156,7 @@ private constructor( LaunchOptions( acceptDownloads, (args ?: JsonMissing.of()).map { it.toImmutable() }, + cdpHeaders, cdpUrl, chromiumSandbox, connectTimeoutMs, @@ -2150,6 +2187,7 @@ private constructor( acceptDownloads() args() + cdpHeaders().ifPresent { it.validate() } cdpUrl() chromiumSandbox() connectTimeoutMs() @@ -2188,6 +2226,7 @@ private constructor( internal fun validity(): Int = (if (acceptDownloads.asKnown().isPresent) 1 else 0) + (args.asKnown().getOrNull()?.size ?: 0) + + (cdpHeaders.asKnown().getOrNull()?.validity() ?: 0) + (if (cdpUrl.asKnown().isPresent) 1 else 0) + (if (chromiumSandbox.asKnown().isPresent) 1 else 0) + (if (connectTimeoutMs.asKnown().isPresent) 1 else 0) + @@ -2206,6 +2245,110 @@ private constructor( (if (userDataDir.asKnown().isPresent) 1 else 0) + (viewport.asKnown().getOrNull()?.validity() ?: 0) + class CdpHeaders + @JsonCreator + private constructor( + @com.fasterxml.jackson.annotation.JsonValue + private val additionalProperties: Map + ) { + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = additionalProperties + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [CdpHeaders]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [CdpHeaders]. */ + class Builder internal constructor() { + + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(cdpHeaders: CdpHeaders) = apply { + additionalProperties = cdpHeaders.additionalProperties.toMutableMap() + } + + fun additionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.clear() + putAllAdditionalProperties(additionalProperties) + } + + fun putAdditionalProperty(key: String, value: JsonValue) = apply { + additionalProperties.put(key, value) + } + + fun putAllAdditionalProperties(additionalProperties: Map) = + apply { + this.additionalProperties.putAll(additionalProperties) + } + + fun removeAdditionalProperty(key: String) = apply { + additionalProperties.remove(key) + } + + fun removeAllAdditionalProperties(keys: Set) = apply { + keys.forEach(::removeAdditionalProperty) + } + + /** + * Returns an immutable instance of [CdpHeaders]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): CdpHeaders = CdpHeaders(additionalProperties.toImmutable()) + } + + private var validated: Boolean = false + + fun validate(): CdpHeaders = apply { + if (validated) { + return@apply + } + + validated = true + } + + fun isValid(): Boolean = + try { + validate() + true + } catch (e: StagehandInvalidDataException) { + false + } + + /** + * Returns a score indicating how many valid values are contained in this object + * recursively. + * + * Used for best match union deserialization. + */ + @JvmSynthetic + internal fun validity(): Int = + additionalProperties.count { (_, value) -> + !value.isNull() && !value.isMissing() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is CdpHeaders && additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = "CdpHeaders{additionalProperties=$additionalProperties}" + } + @JsonDeserialize(using = IgnoreDefaultArgs.Deserializer::class) @JsonSerialize(using = IgnoreDefaultArgs.Serializer::class) class IgnoreDefaultArgs @@ -2872,6 +3015,7 @@ private constructor( return other is LaunchOptions && acceptDownloads == other.acceptDownloads && args == other.args && + cdpHeaders == other.cdpHeaders && cdpUrl == other.cdpUrl && chromiumSandbox == other.chromiumSandbox && connectTimeoutMs == other.connectTimeoutMs && @@ -2896,6 +3040,7 @@ private constructor( Objects.hash( acceptDownloads, args, + cdpHeaders, cdpUrl, chromiumSandbox, connectTimeoutMs, @@ -2920,7 +3065,7 @@ private constructor( override fun hashCode(): Int = hashCode override fun toString() = - "LaunchOptions{acceptDownloads=$acceptDownloads, args=$args, cdpUrl=$cdpUrl, chromiumSandbox=$chromiumSandbox, connectTimeoutMs=$connectTimeoutMs, deviceScaleFactor=$deviceScaleFactor, devtools=$devtools, downloadsPath=$downloadsPath, executablePath=$executablePath, hasTouch=$hasTouch, headless=$headless, ignoreDefaultArgs=$ignoreDefaultArgs, ignoreHttpsErrors=$ignoreHttpsErrors, locale=$locale, port=$port, preserveUserDataDir=$preserveUserDataDir, proxy=$proxy, userDataDir=$userDataDir, viewport=$viewport, additionalProperties=$additionalProperties}" + "LaunchOptions{acceptDownloads=$acceptDownloads, args=$args, cdpHeaders=$cdpHeaders, cdpUrl=$cdpUrl, chromiumSandbox=$chromiumSandbox, connectTimeoutMs=$connectTimeoutMs, deviceScaleFactor=$deviceScaleFactor, devtools=$devtools, downloadsPath=$downloadsPath, executablePath=$executablePath, hasTouch=$hasTouch, headless=$headless, ignoreDefaultArgs=$ignoreDefaultArgs, ignoreHttpsErrors=$ignoreHttpsErrors, locale=$locale, port=$port, preserveUserDataDir=$preserveUserDataDir, proxy=$proxy, userDataDir=$userDataDir, viewport=$viewport, additionalProperties=$additionalProperties}" } /** Browser type to use */ diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/StreamEvent.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/StreamEvent.kt index 443104b..4584374 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/StreamEvent.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/StreamEvent.kt @@ -30,8 +30,8 @@ import java.util.Optional import kotlin.jvm.optionals.getOrNull /** - * Server-Sent Event emitted during streaming responses. Events are sent as `data: \n\n`. Key - * order: data (with status first), type, id. + * Server-Sent Event emitted during streaming responses. Events are sent as `event: \ndata: + * \n\n`, where the JSON payload has the shape `{ data, type, id }`. */ class StreamEvent @JsonCreator(mode = JsonCreator.Mode.DISABLED) diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ClientOptionsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ClientOptionsTest.kt index 75d56e8..1bd88a6 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ClientOptionsTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ClientOptionsTest.kt @@ -16,6 +16,37 @@ internal class ClientOptionsTest { private val httpClient = mock() + @Test + fun putHeader_canOverwriteDefaultHeader() { + val clientOptions = + ClientOptions.builder() + .httpClient(httpClient) + .putHeader("User-Agent", "My User Agent") + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + + assertThat(clientOptions.headers.values("User-Agent")).containsExactly("My User Agent") + } + + @Test + fun toBuilder_bbApiKeyAuthCanBeUpdated() { + var clientOptions = + ClientOptions.builder() + .httpClient(httpClient) + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + + clientOptions = + clientOptions.toBuilder().browserbaseApiKey("another My Browserbase API Key").build() + + assertThat(clientOptions.headers.values("x-bb-api-key")) + .containsExactly("another My Browserbase API Key") + } + @Test fun toBuilder_whenOriginalClientOptionsGarbageCollected_doesNotCloseOriginalClient() { var clientOptions = diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/handlers/SseHandlerTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/handlers/SseHandlerTest.kt index 29105f3..2e4dfb0 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/handlers/SseHandlerTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/handlers/SseHandlerTest.kt @@ -2,10 +2,12 @@ package com.browserbase.api.core.handlers +import com.browserbase.api.core.JsonValue import com.browserbase.api.core.http.Headers import com.browserbase.api.core.http.HttpResponse import com.browserbase.api.core.http.SseMessage import com.browserbase.api.core.jsonMapper +import com.browserbase.api.errors.SseException import java.io.InputStream import java.util.stream.Collectors.toList import org.assertj.core.api.Assertions.assertThat @@ -21,64 +23,105 @@ internal class SseHandlerTest { internal val expectedMessages: List? = null, internal val expectedException: Exception? = null, ) { - DATA_MISSING_EVENT( + EVENT_AND_DATA( buildString { + append("event: starting\n") append("data: {\"foo\":true}\n") append("\n") }, - listOf(sseMessageBuilder().data("{\"foo\":true}").build()), + listOf(sseMessageBuilder().event("starting").data("{\"foo\":true}").build()), ), - MULTIPLE_DATA_MISSING_EVENT( + EVENT_MISSING_DATA( buildString { + append("event: starting\n") + append("\n") + }, + listOf(sseMessageBuilder().event("starting").build()), + ), + MULTIPLE_EVENTS_AND_DATA( + buildString { + append("event: starting\n") append("data: {\"foo\":true}\n") append("\n") + append("event: connected\n") append("data: {\"bar\":false}\n") append("\n") }, listOf( - sseMessageBuilder().data("{\"foo\":true}").build(), - sseMessageBuilder().data("{\"bar\":false}").build(), + sseMessageBuilder().event("starting").data("{\"foo\":true}").build(), + sseMessageBuilder().event("connected").data("{\"bar\":false}").build(), + ), + ), + MULTIPLE_EVENTS_MISSING_DATA( + buildString { + append("event: starting\n") + append("\n") + append("event: connected\n") + append("\n") + }, + listOf( + sseMessageBuilder().event("starting").build(), + sseMessageBuilder().event("connected").build(), ), ), DATA_JSON_ESCAPED_DOUBLE_NEW_LINE( buildString { + append("event: starting\n") append("data: {\n") append("data: \"foo\":\n") append("data: true}\n") append("\n\n") }, - listOf(sseMessageBuilder().data("{\n\"foo\":\ntrue}").build()), + listOf(sseMessageBuilder().event("starting").data("{\n\"foo\":\ntrue}").build()), ), MULTIPLE_DATA_LINES( buildString { + append("event: starting\n") append("data: {\n") append("data: \"foo\":\n") append("data: true}\n") append("\n\n") }, - listOf(sseMessageBuilder().data("{\n\"foo\":\ntrue}").build()), + listOf(sseMessageBuilder().event("starting").data("{\n\"foo\":\ntrue}").build()), ), SPECIAL_NEW_LINE_CHARACTER( buildString { + append("event: starting\n") append("data: {\"content\":\" culpa\"}\n") append("\n") + append("event: connected\n") append("data: {\"content\":\" \u2028\"}\n") append("\n") + append("event: starting\n") append("data: {\"content\":\"foo\"}\n") append("\n") }, listOf( - sseMessageBuilder().data("{\"content\":\" culpa\"}").build(), - sseMessageBuilder().data("{\"content\":\" \u2028\"}").build(), - sseMessageBuilder().data("{\"content\":\"foo\"}").build(), + sseMessageBuilder().event("starting").data("{\"content\":\" culpa\"}").build(), + sseMessageBuilder().event("connected").data("{\"content\":\" \u2028\"}").build(), + sseMessageBuilder().event("starting").data("{\"content\":\"foo\"}").build(), ), ), MULTI_BYTE_CHARACTER( buildString { + append("event: starting\n") append("data: {\"content\":\"\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0438\"}\n") append("\n") }, - listOf(sseMessageBuilder().data("{\"content\":\"известни\"}").build()), + listOf(sseMessageBuilder().event("starting").data("{\"content\":\"известни\"}").build()), + ), + ERROR_EVENT( + buildString { + append("event: error\n") + append("data: {\"errorProperty\":\"42\"}\n") + append("\n") + }, + expectedException = + SseException.builder() + .statusCode(0) + .headers(Headers.builder().build()) + .body(JsonValue.from(mapOf("errorProperty" to "42"))) + .build(), ), } diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/RetryingHttpClientTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/RetryingHttpClientTest.kt index ce14c91..49e0138 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/RetryingHttpClientTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/RetryingHttpClientTest.kt @@ -20,7 +20,11 @@ import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo import com.github.tomakehurst.wiremock.junit5.WireMockTest import com.github.tomakehurst.wiremock.stubbing.Scenario import java.io.InputStream +import java.time.Clock import java.time.Duration +import java.time.OffsetDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter import java.util.concurrent.CompletableFuture import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach @@ -36,6 +40,21 @@ internal class RetryingHttpClientTest { private lateinit var baseUrl: String private lateinit var httpClient: HttpClient + private class RecordingSleeper : Sleeper { + val durations = mutableListOf() + + override fun sleep(duration: Duration) { + durations.add(duration) + } + + override fun sleepAsync(duration: Duration): CompletableFuture { + durations.add(duration) + return CompletableFuture.completedFuture(null) + } + + override fun close() {} + } + @BeforeEach fun beforeEach(wmRuntimeInfo: WireMockRuntimeInfo) { baseUrl = wmRuntimeInfo.httpBaseUrl @@ -86,7 +105,8 @@ internal class RetryingHttpClientTest { @ValueSource(booleans = [false, true]) fun execute(async: Boolean) { stubFor(post(urlPathEqualTo("/something")).willReturn(ok())) - val retryingClient = retryingHttpClientBuilder().build() + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).build() val response = retryingClient.execute( @@ -100,6 +120,7 @@ internal class RetryingHttpClientTest { assertThat(response.statusCode()).isEqualTo(200) verify(1, postRequestedFor(urlPathEqualTo("/something"))) + assertThat(sleeper.durations).isEmpty() assertNoResponseLeaks() } @@ -111,8 +132,12 @@ internal class RetryingHttpClientTest { .withHeader("X-Some-Header", matching("stainless-java-retry-.+")) .willReturn(ok()) ) + val sleeper = RecordingSleeper() val retryingClient = - retryingHttpClientBuilder().maxRetries(2).idempotencyHeader("X-Some-Header").build() + retryingHttpClientBuilder(sleeper) + .maxRetries(2) + .idempotencyHeader("X-Some-Header") + .build() val response = retryingClient.execute( @@ -126,20 +151,20 @@ internal class RetryingHttpClientTest { assertThat(response.statusCode()).isEqualTo(200) verify(1, postRequestedFor(urlPathEqualTo("/something"))) + assertThat(sleeper.durations).isEmpty() assertNoResponseLeaks() } @ParameterizedTest @ValueSource(booleans = [false, true]) fun execute_withRetryAfterHeader(async: Boolean) { + val retryAfterDate = "Wed, 21 Oct 2015 07:28:00 GMT" stubFor( post(urlPathEqualTo("/something")) // First we fail with a retry after header given as a date .inScenario("foo") .whenScenarioStateIs(Scenario.STARTED) - .willReturn( - serviceUnavailable().withHeader("Retry-After", "Wed, 21 Oct 2015 07:28:00 GMT") - ) + .willReturn(serviceUnavailable().withHeader("Retry-After", retryAfterDate)) .willSetStateTo("RETRY_AFTER_DATE") ) stubFor( @@ -158,7 +183,13 @@ internal class RetryingHttpClientTest { .willReturn(ok()) .willSetStateTo("COMPLETED") ) - val retryingClient = retryingHttpClientBuilder().maxRetries(2).build() + // Fix the clock to 5 seconds before the Retry-After date so the date-based backoff is + // deterministic. + val retryAfterDateTime = + OffsetDateTime.parse(retryAfterDate, DateTimeFormatter.RFC_1123_DATE_TIME) + val clock = Clock.fixed(retryAfterDateTime.minusSeconds(5).toInstant(), ZoneOffset.UTC) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper, clock).maxRetries(2).build() val response = retryingClient.execute( @@ -186,19 +217,20 @@ internal class RetryingHttpClientTest { postRequestedFor(urlPathEqualTo("/something")) .withHeader("x-stainless-retry-count", equalTo("2")), ) + assertThat(sleeper.durations) + .containsExactly(Duration.ofSeconds(5), Duration.ofMillis(1234)) assertNoResponseLeaks() } @ParameterizedTest @ValueSource(booleans = [false, true]) fun execute_withOverwrittenRetryCountHeader(async: Boolean) { + val retryAfterDate = "Wed, 21 Oct 2015 07:28:00 GMT" stubFor( post(urlPathEqualTo("/something")) .inScenario("foo") // first we fail with a retry after header given as a date .whenScenarioStateIs(Scenario.STARTED) - .willReturn( - serviceUnavailable().withHeader("Retry-After", "Wed, 21 Oct 2015 07:28:00 GMT") - ) + .willReturn(serviceUnavailable().withHeader("Retry-After", retryAfterDate)) .willSetStateTo("RETRY_AFTER_DATE") ) stubFor( @@ -208,7 +240,11 @@ internal class RetryingHttpClientTest { .willReturn(ok()) .willSetStateTo("COMPLETED") ) - val retryingClient = retryingHttpClientBuilder().maxRetries(2).build() + val retryAfterDateTime = + OffsetDateTime.parse(retryAfterDate, DateTimeFormatter.RFC_1123_DATE_TIME) + val clock = Clock.fixed(retryAfterDateTime.minusSeconds(5).toInstant(), ZoneOffset.UTC) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper, clock).maxRetries(2).build() val response = retryingClient.execute( @@ -227,6 +263,7 @@ internal class RetryingHttpClientTest { postRequestedFor(urlPathEqualTo("/something")) .withHeader("x-stainless-retry-count", equalTo("42")), ) + assertThat(sleeper.durations).containsExactly(Duration.ofSeconds(5)) assertNoResponseLeaks() } @@ -247,7 +284,8 @@ internal class RetryingHttpClientTest { .willReturn(ok()) .willSetStateTo("COMPLETED") ) - val retryingClient = retryingHttpClientBuilder().maxRetries(1).build() + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(1).build() val response = retryingClient.execute( @@ -261,6 +299,7 @@ internal class RetryingHttpClientTest { assertThat(response.statusCode()).isEqualTo(200) verify(2, postRequestedFor(urlPathEqualTo("/something"))) + assertThat(sleeper.durations).containsExactly(Duration.ofMillis(10)) assertNoResponseLeaks() } @@ -301,21 +340,12 @@ internal class RetryingHttpClientTest { override fun close() = httpClient.close() } + val sleeper = RecordingSleeper() val retryingClient = RetryingHttpClient.builder() .httpClient(failingHttpClient) .maxRetries(2) - .sleeper( - object : Sleeper { - - override fun sleep(duration: Duration) {} - - override fun sleepAsync(duration: Duration): CompletableFuture = - CompletableFuture.completedFuture(null) - - override fun close() {} - } - ) + .sleeper(sleeper) .build() val response = @@ -339,25 +369,153 @@ internal class RetryingHttpClientTest { postRequestedFor(urlPathEqualTo("/something")) .withHeader("x-stainless-retry-count", equalTo("0")), ) + // Exponential backoff with jitter: 0.5s * jitter where jitter is in [0.75, 1.0]. + assertThat(sleeper.durations).hasSize(1) + assertThat(sleeper.durations[0]).isBetween(Duration.ofMillis(375), Duration.ofMillis(500)) assertNoResponseLeaks() } - private fun retryingHttpClientBuilder() = - RetryingHttpClient.builder() - .httpClient(httpClient) - // Use a no-op `Sleeper` to make the test fast. - .sleeper( - object : Sleeper { + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withExponentialBackoff(async: Boolean) { + stubFor(post(urlPathEqualTo("/something")).willReturn(serviceUnavailable())) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(3).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) - override fun sleep(duration: Duration) {} + // All retries exhausted; the last 503 response is returned. + assertThat(response.statusCode()).isEqualTo(503) + verify(4, postRequestedFor(urlPathEqualTo("/something"))) + // Exponential backoff with jitter: backoff = min(0.5 * 2^(retries-1), 8) * jitter where + // jitter is in [0.75, 1.0]. + assertThat(sleeper.durations).hasSize(3) + // retries=1: 0.5s * [0.75, 1.0] + assertThat(sleeper.durations[0]).isBetween(Duration.ofMillis(375), Duration.ofMillis(500)) + // retries=2: 1s * [0.75, 1.0] + assertThat(sleeper.durations[1]).isBetween(Duration.ofMillis(750), Duration.ofMillis(1000)) + // retries=3: 2s * [0.75, 1.0] + assertThat(sleeper.durations[2]).isBetween(Duration.ofMillis(1500), Duration.ofMillis(2000)) + assertNoResponseLeaks() + } - override fun sleepAsync(duration: Duration): CompletableFuture = - CompletableFuture.completedFuture(null) + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withExponentialBackoffCap(async: Boolean) { + stubFor(post(urlPathEqualTo("/something")).willReturn(serviceUnavailable())) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(6).build() - override fun close() {} - } + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, ) + assertThat(response.statusCode()).isEqualTo(503) + verify(7, postRequestedFor(urlPathEqualTo("/something"))) + assertThat(sleeper.durations).hasSize(6) + // retries=5: backoff hits the 8s cap * [0.75, 1.0] + assertThat(sleeper.durations[4]).isBetween(Duration.ofMillis(6000), Duration.ofMillis(8000)) + // retries=6: still capped at 8s * [0.75, 1.0] + assertThat(sleeper.durations[5]).isBetween(Duration.ofMillis(6000), Duration.ofMillis(8000)) + assertNoResponseLeaks() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withRetryAfterMsPriorityOverRetryAfter(async: Boolean) { + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn( + serviceUnavailable() + .withHeader("Retry-After-Ms", "50") + .withHeader("Retry-After", "2") + ) + .willSetStateTo("RETRY") + ) + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") + .whenScenarioStateIs("RETRY") + .willReturn(ok()) + .willSetStateTo("COMPLETED") + ) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(1).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(200) + // Retry-After-Ms (50ms) takes priority over Retry-After (2s). + assertThat(sleeper.durations).containsExactly(Duration.ofMillis(50)) + assertNoResponseLeaks() + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun execute_withRetryAfterUnparseable(async: Boolean) { + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn(serviceUnavailable().withHeader("Retry-After", "not-a-date-or-number")) + .willSetStateTo("RETRY") + ) + stubFor( + post(urlPathEqualTo("/something")) + .inScenario("foo") + .whenScenarioStateIs("RETRY") + .willReturn(ok()) + .willSetStateTo("COMPLETED") + ) + val sleeper = RecordingSleeper() + val retryingClient = retryingHttpClientBuilder(sleeper).maxRetries(1).build() + + val response = + retryingClient.execute( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl(baseUrl) + .addPathSegment("something") + .build(), + async, + ) + + assertThat(response.statusCode()).isEqualTo(200) + // Unparseable Retry-After falls through to exponential backoff. + assertThat(sleeper.durations).hasSize(1) + assertThat(sleeper.durations[0]).isBetween(Duration.ofMillis(375), Duration.ofMillis(500)) + assertNoResponseLeaks() + } + + private fun retryingHttpClientBuilder( + sleeper: RecordingSleeper, + clock: Clock = Clock.systemUTC(), + ) = RetryingHttpClient.builder().httpClient(httpClient).sleeper(sleeper).clock(clock) + private fun HttpClient.execute(request: HttpRequest, async: Boolean): HttpResponse = if (async) executeAsync(request).get() else execute(request) diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/ModelConfigTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/ModelConfigTest.kt index cd7f6b7..ee341a1 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/ModelConfigTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/ModelConfigTest.kt @@ -2,6 +2,7 @@ package com.browserbase.api.models.sessions +import com.browserbase.api.core.JsonValue import com.browserbase.api.core.jsonMapper import com.fasterxml.jackson.module.kotlin.jacksonTypeRef import org.assertj.core.api.Assertions.assertThat @@ -16,12 +17,23 @@ internal class ModelConfigTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() assertThat(modelConfig.modelName()).isEqualTo("openai/gpt-5-nano") assertThat(modelConfig.apiKey()).contains("sk-some-openai-api-key") assertThat(modelConfig.baseUrl()).contains("https://api.openai.com/v1") + assertThat(modelConfig.headers()) + .contains( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) assertThat(modelConfig.provider()).contains(ModelConfig.Provider.OPENAI) } @@ -33,6 +45,11 @@ internal class ModelConfigTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionActParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionActParamsTest.kt index 9d1e452..86e9f29 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionActParamsTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionActParamsTest.kt @@ -23,6 +23,11 @@ internal class SessionActParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -30,6 +35,15 @@ internal class SessionActParamsTest { .variables( SessionActParams.Options.Variables.builder() .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) .build() ) .build() @@ -65,6 +79,11 @@ internal class SessionActParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -72,6 +91,15 @@ internal class SessionActParamsTest { .variables( SessionActParams.Options.Variables.builder() .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) .build() ) .build() @@ -111,6 +139,11 @@ internal class SessionActParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -118,6 +151,15 @@ internal class SessionActParamsTest { .variables( SessionActParams.Options.Variables.builder() .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) .build() ) .build() @@ -137,6 +179,11 @@ internal class SessionActParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -144,6 +191,15 @@ internal class SessionActParamsTest { .variables( SessionActParams.Options.Variables.builder() .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) .build() ) .build() diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteParamsTest.kt index cb26cb2..dc042b7 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteParamsTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteParamsTest.kt @@ -2,6 +2,7 @@ package com.browserbase.api.models.sessions +import com.browserbase.api.core.JsonValue import com.browserbase.api.core.http.Headers import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -21,6 +22,11 @@ internal class SessionExecuteParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -30,6 +36,11 @@ internal class SessionExecuteParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -44,6 +55,8 @@ internal class SessionExecuteParamsTest { ) .highlightCursor(true) .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) .build() ) .frameId("frameId") @@ -85,6 +98,11 @@ internal class SessionExecuteParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -94,6 +112,11 @@ internal class SessionExecuteParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -108,6 +131,8 @@ internal class SessionExecuteParamsTest { ) .highlightCursor(true) .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) .build() ) .frameId("frameId") @@ -153,6 +178,11 @@ internal class SessionExecuteParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -162,6 +192,11 @@ internal class SessionExecuteParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -176,6 +211,8 @@ internal class SessionExecuteParamsTest { ) .highlightCursor(true) .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) .build() ) .frameId("frameId") @@ -193,6 +230,11 @@ internal class SessionExecuteParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -202,6 +244,11 @@ internal class SessionExecuteParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -217,6 +264,8 @@ internal class SessionExecuteParamsTest { ) .highlightCursor(true) .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) .build() ) assertThat(body.frameId()).contains("frameId") diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExtractParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExtractParamsTest.kt index 3061c8d..1418be5 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExtractParamsTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExtractParamsTest.kt @@ -23,6 +23,11 @@ internal class SessionExtractParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -63,6 +68,11 @@ internal class SessionExtractParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -107,6 +117,11 @@ internal class SessionExtractParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -134,6 +149,11 @@ internal class SessionExtractParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionObserveParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionObserveParamsTest.kt index f238c0e..1ed125e 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionObserveParamsTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionObserveParamsTest.kt @@ -2,6 +2,7 @@ package com.browserbase.api.models.sessions +import com.browserbase.api.core.JsonValue import com.browserbase.api.core.http.Headers import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -22,11 +23,30 @@ internal class SessionObserveParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) .selector("nav") .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) .build() ) .build() @@ -57,11 +77,30 @@ internal class SessionObserveParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) .selector("nav") .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) .build() ) .build() @@ -96,11 +135,30 @@ internal class SessionObserveParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) .selector("nav") .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) .build() ) .build() @@ -117,11 +175,30 @@ internal class SessionObserveParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) .selector("nav") .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) .build() ) } diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionStartParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionStartParamsTest.kt index e22e941..5b35aaa 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionStartParamsTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionStartParamsTest.kt @@ -22,6 +22,11 @@ internal class SessionStartParamsTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -168,6 +173,11 @@ internal class SessionStartParamsTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -331,6 +341,11 @@ internal class SessionStartParamsTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -478,6 +493,11 @@ internal class SessionStartParamsTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ErrorHandlingTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ErrorHandlingTest.kt index a28b993..a123b5c 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ErrorHandlingTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ErrorHandlingTest.kt @@ -84,6 +84,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -261,6 +270,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -438,6 +456,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -615,6 +642,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -792,6 +828,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -969,6 +1014,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -1146,6 +1200,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -1323,6 +1386,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -1500,6 +1572,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -1677,6 +1758,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -1854,6 +1944,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -2031,6 +2130,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -2208,6 +2316,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -2385,6 +2502,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -2562,6 +2688,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -2739,6 +2874,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -2914,6 +3058,15 @@ internal class ErrorHandlingTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty( + "foo", + JsonValue.from("string"), + ) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ServiceParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ServiceParamsTest.kt index f3a40b7..3f10c1a 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ServiceParamsTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/ServiceParamsTest.kt @@ -58,6 +58,11 @@ internal class ServiceParamsTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) @@ -224,6 +229,11 @@ internal class ServiceParamsTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -231,6 +241,15 @@ internal class ServiceParamsTest { .variables( SessionActParams.Options.Variables.builder() .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) .build() ) .build() diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/async/SessionServiceAsyncTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/async/SessionServiceAsyncTest.kt index 2a0e601..471a7ac 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/async/SessionServiceAsyncTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/async/SessionServiceAsyncTest.kt @@ -43,6 +43,11 @@ internal class SessionServiceAsyncTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -50,6 +55,15 @@ internal class SessionServiceAsyncTest { .variables( SessionActParams.Options.Variables.builder() .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) .build() ) .build() @@ -86,6 +100,11 @@ internal class SessionServiceAsyncTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -93,6 +112,15 @@ internal class SessionServiceAsyncTest { .variables( SessionActParams.Options.Variables.builder() .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) .build() ) .build() @@ -152,6 +180,11 @@ internal class SessionServiceAsyncTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -161,6 +194,11 @@ internal class SessionServiceAsyncTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -175,6 +213,8 @@ internal class SessionServiceAsyncTest { ) .highlightCursor(true) .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) .build() ) .frameId("frameId") @@ -210,6 +250,11 @@ internal class SessionServiceAsyncTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -219,6 +264,11 @@ internal class SessionServiceAsyncTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -233,6 +283,8 @@ internal class SessionServiceAsyncTest { ) .highlightCursor(true) .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) .build() ) .frameId("frameId") @@ -270,6 +322,11 @@ internal class SessionServiceAsyncTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -314,6 +371,11 @@ internal class SessionServiceAsyncTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -392,11 +454,30 @@ internal class SessionServiceAsyncTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) .selector("nav") .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) .build() ) .build() @@ -431,11 +512,30 @@ internal class SessionServiceAsyncTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) .selector("nav") .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) .build() ) .build() @@ -493,6 +593,12 @@ internal class SessionServiceAsyncTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0) diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/blocking/SessionServiceTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/blocking/SessionServiceTest.kt index dedab31..e6c5cb5 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/blocking/SessionServiceTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/blocking/SessionServiceTest.kt @@ -43,6 +43,11 @@ internal class SessionServiceTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -50,6 +55,15 @@ internal class SessionServiceTest { .variables( SessionActParams.Options.Variables.builder() .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) .build() ) .build() @@ -85,6 +99,11 @@ internal class SessionServiceTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -92,6 +111,15 @@ internal class SessionServiceTest { .variables( SessionActParams.Options.Variables.builder() .putAdditionalProperty("username", JsonValue.from("john_doe")) + .putAdditionalProperty( + "password", + JsonValue.from( + mapOf( + "value" to "secret123", + "description" to "The login password", + ) + ), + ) .build() ) .build() @@ -150,6 +178,11 @@ internal class SessionServiceTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -159,6 +192,11 @@ internal class SessionServiceTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -173,6 +211,8 @@ internal class SessionServiceTest { ) .highlightCursor(true) .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) .build() ) .frameId("frameId") @@ -207,6 +247,11 @@ internal class SessionServiceTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -216,6 +261,11 @@ internal class SessionServiceTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -230,6 +280,8 @@ internal class SessionServiceTest { ) .highlightCursor(true) .maxSteps(20.0) + .toolTimeout(30000.0) + .useSearch(true) .build() ) .frameId("frameId") @@ -267,6 +319,11 @@ internal class SessionServiceTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -310,6 +367,11 @@ internal class SessionServiceTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) @@ -387,11 +449,30 @@ internal class SessionServiceTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) .selector("nav") .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) .build() ) .build() @@ -425,11 +506,30 @@ internal class SessionServiceTest { .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") + .headers( + ModelConfig.Headers.builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .provider(ModelConfig.Provider.OPENAI) .build() ) .selector("nav") .timeout(30000.0) + .variables( + SessionObserveParams.Options.Variables.builder() + .putAdditionalProperty( + "username", + JsonValue.from( + mapOf( + "value" to "john@example.com", + "description" to "The login email", + ) + ), + ) + .putAdditionalProperty("rememberMe", JsonValue.from(true)) + .build() + ) .build() ) .build() @@ -486,6 +586,12 @@ internal class SessionServiceTest { SessionStartParams.Browser.LaunchOptions.builder() .acceptDownloads(true) .addArg("string") + .cdpHeaders( + SessionStartParams.Browser.LaunchOptions.CdpHeaders + .builder() + .putAdditionalProperty("foo", JsonValue.from("string")) + .build() + ) .cdpUrl("cdpUrl") .chromiumSandbox(true) .connectTimeoutMs(0.0)