diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e136c7c..27fda9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,10 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | @@ -47,10 +47,10 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | @@ -67,7 +67,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/stagehand-java' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -85,10 +85,10 @@ jobs: 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 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml index 6a27d98..e0e380c 100644 --- a/.github/workflows/publish-sonatype.yml +++ b/.github/workflows/publish-sonatype.yml @@ -14,10 +14,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | @@ -33,7 +33,7 @@ jobs: export -- GPG_SIGNING_KEY_ID printenv -- GPG_SIGNING_KEY | gpg --batch --passphrase-fd 3 --import 3<<< "$GPG_SIGNING_PASSWORD" GPG_SIGNING_KEY_ID="$(gpg --with-colons --list-keys | awk -F : -- '/^pub:/ { getline; print "0x" substr($10, length($10) - 7) }')" - ./gradlew publish --no-configuration-cache + ./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD" --no-configuration-cache env: SONATYPE_USERNAME: ${{ secrets.STAGEHAND_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.STAGEHAND_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index a6e0235..e71595c 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'browserbase/stagehand-java' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ac03171..1b77f50 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.1" + ".": "0.7.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index a03dd75..a0da57a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-419940ce988c43313660d30a5bb5b5c2d89b3b19a0f80fe050331e0f4e8c58d2.yml -openapi_spec_hash: a621ca69697ebba7286cbf9e475c46ad -config_hash: 74111faa0876db6b053526281c444498 +configured_endpoints: 8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-8fbb3fa8f3a37c1c7408de427fe125aadec49f705e8e30d191601a9b69c4cc41.yml +openapi_spec_hash: 8a36f79075102c63234ed06107deb8c9 +config_hash: 7386d24e2f03a3b2a89b3f6881446348 diff --git a/CHANGELOG.md b/CHANGELOG.md index fa73cdd..58f0478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,55 @@ # Changelog +## 0.7.0 (2026-02-04) + +Full Changelog: [v0.6.1...v0.7.0](https://github.com/browserbase/stagehand-java/compare/v0.6.1...v0.7.0) + +### Features + +* [feat]: add support for local caching of agent when using api (2) ([3d9f1bb](https://github.com/browserbase/stagehand-java/commit/3d9f1bb130e77142e24dd4d0c7426a87df65e21a)) +* add auto-bedrock support based on bedrock/provider.model-name ([d2ae617](https://github.com/browserbase/stagehand-java/commit/d2ae6176a35c30107c3eac7dfe6e73a1e11a1684)) +* Add executionModel serialization to api client ([2628417](https://github.com/browserbase/stagehand-java/commit/2628417132606dab5289282976ba4e9ef25b829d)) +* add v3 integration tests matching cloud exactly ([769ed85](https://github.com/browserbase/stagehand-java/commit/769ed852faede431af5cef830df999d8eeaa24fa)) +* **api:** manual updates ([007f9c7](https://github.com/browserbase/stagehand-java/commit/007f9c77a3ead3279c2a2432cbcd4c9cd365b386)) +* **api:** manual updates ([cb5323a](https://github.com/browserbase/stagehand-java/commit/cb5323a5034a25ed82b7a09eb010542d5bad9d9c)) +* **api:** manual updates ([ede49b6](https://github.com/browserbase/stagehand-java/commit/ede49b6c8137a59bcbfc1d2cc82a0fdda805ea11)) +* **client:** send `X-Stainless-Kotlin-Version` header ([988e6c8](https://github.com/browserbase/stagehand-java/commit/988e6c8dc90bab42c243684a1d3a57e33a393861)) +* End endpoint cleanup ([10ccbd9](https://github.com/browserbase/stagehand-java/commit/10ccbd915a545b6c35c95adada8c834808a35b88)) +* Include replay endpoint in stainless spec so SDK clients can get run metrics ([e307cff](https://github.com/browserbase/stagehand-java/commit/e307cff5f7c2d63ca4b84f40d4656e5690dda7e3)) +* move Stainless compatibility transforms from gen-openapi.ts into stainless.yml ([57d6b94](https://github.com/browserbase/stagehand-java/commit/57d6b94a1391ebd8b8023063f78d5b76bf91b65c)) +* Removed MCP from readme for now ([2b8ab62](https://github.com/browserbase/stagehand-java/commit/2b8ab629120e0733755b598d0050e6c5af1a47b9)) +* Update stainless.yml for project and publish settings ([65820a9](https://github.com/browserbase/stagehand-java/commit/65820a92b5569954133f10962c38ac85015aeda2)) +* x-stainless-any fix, optional frame id, ModelConfigString fix ([ec581cf](https://github.com/browserbase/stagehand-java/commit/ec581cf6dcb166547521e8e64b1239876fd5a527)) + + +### Bug Fixes + +* **client:** disallow coercion from float to int ([012dbc8](https://github.com/browserbase/stagehand-java/commit/012dbc823262bf9707c4bac7ec161c79a0592e97)) +* **client:** fully respect max retries ([f8b09f0](https://github.com/browserbase/stagehand-java/commit/f8b09f098ce22d6e8dce3f46ff67cc199f3e10ab)) +* **client:** preserve time zone in lenient date-time parsing ([934b350](https://github.com/browserbase/stagehand-java/commit/934b350a93f668b988d465580a3f5cc0d1be8d37)) +* **client:** send retry count header for max retries 0 ([f8b09f0](https://github.com/browserbase/stagehand-java/commit/f8b09f098ce22d6e8dce3f46ff67cc199f3e10ab)) +* date time deserialization leniency ([63d6c36](https://github.com/browserbase/stagehand-java/commit/63d6c361ba8510cedd21bbf355881dd603ff06f1)) +* **docs:** fix mcp installation instructions for remote servers ([793e911](https://github.com/browserbase/stagehand-java/commit/793e911f5e1bd911a5c92de375c3a158b94da45b)) + + +### Chores + +* **ci:** upgrade `actions/github-script` ([4aa63b9](https://github.com/browserbase/stagehand-java/commit/4aa63b9de7d588e226a6b8de221daf152a4978dc)) +* **ci:** upgrade `actions/setup-java` ([8b3f116](https://github.com/browserbase/stagehand-java/commit/8b3f116aff59f0a870c8bb55410fcd37c122b708)) +* **internal:** allow passing args to `./scripts/test` ([f771390](https://github.com/browserbase/stagehand-java/commit/f771390dc0796c154ad877c07d4e488192f40bc1)) +* **internal:** clean up maven repo artifact script and add html documentation to repo root ([2918f13](https://github.com/browserbase/stagehand-java/commit/2918f1366c3193d21548dec899d3ae053dbd350a)) +* **internal:** correct cache invalidation for `SKIP_MOCK_TESTS` ([b407a38](https://github.com/browserbase/stagehand-java/commit/b407a387d4131ab1a3e335dea7ed40501d327605)) +* **internal:** depend on packages directly in example ([f8b09f0](https://github.com/browserbase/stagehand-java/commit/f8b09f098ce22d6e8dce3f46ff67cc199f3e10ab)) +* **internal:** improve maven repo docs ([0723676](https://github.com/browserbase/stagehand-java/commit/07236764dcf17210682b14d2f30b41b87a35061c)) +* **internal:** update `actions/checkout` version ([77009b1](https://github.com/browserbase/stagehand-java/commit/77009b1b8512e7eb8f194fbc11564958d92f3028)) +* **internal:** update maven repo doc to include authentication ([2dead90](https://github.com/browserbase/stagehand-java/commit/2dead900d2bf803af18b39d4754e29db132d3849)) +* test on Jackson 2.14.0 to avoid encountering FasterXML/jackson-databind[#3240](https://github.com/browserbase/stagehand-java/issues/3240) in tests ([63d6c36](https://github.com/browserbase/stagehand-java/commit/63d6c361ba8510cedd21bbf355881dd603ff06f1)) + + +### Documentation + +* add comment for arbitrary value fields ([6fa48db](https://github.com/browserbase/stagehand-java/commit/6fa48db3f27bcdc1e2a8255364f205f3823a03c9)) + ## 0.6.1 (2026-01-13) Full Changelog: [v0.6.0...v0.6.1](https://github.com/browserbase/stagehand-java/compare/v0.6.0...v0.6.1) diff --git a/LICENSE b/LICENSE index 6b24314..a7b82c2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,7 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Copyright 2026 stagehand - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - 1. Definitions. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2025 Stagehand - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 8297c8d..cd50594 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ +# Stagehand Java API Library + + + +[![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.0) +[![javadoc](https://javadoc.io/badge2/com.browserbase.api/stagehand-java/0.7.0/javadoc.svg)](https://javadoc.io/doc/com.browserbase.api/stagehand-java/0.7.0) + + + +The Stagehand Java SDK provides convenient access to the [Stagehand REST API](https://docs.stagehand.dev) from applications written in Java.
## What is Stagehand? +The Stagehand Java SDK is similar to the Stagehand Kotlin SDK but with minor differences that make it more ergonomic for use in Java, such as `Optional` instead of nullable values, `Stream` instead of `Sequence`, and `CompletableFuture` instead of suspend functions. + +It is generated with [Stainless](https://www.stainless.com/). Stagehand is a browser automation framework used to control web browsers with natural language and code. By combining the power of AI with the precision of code, Stagehand makes web automation flexible, maintainable, and actually reliable. @@ -70,7 +83,7 @@ Most existing browser automation tools either require you to write low-level cod ### Gradle ```kotlin -implementation("com.browserbase.api:stagehand-java:0.6.1") +implementation("com.browserbase.api:stagehand-java:0.7.0") ``` ### Maven @@ -79,7 +92,7 @@ implementation("com.browserbase.api:stagehand-java:0.6.1") com.browserbase.api stagehand-java - 0.6.1 + 0.7.0 ``` @@ -219,12 +232,10 @@ public class Main { .maxSteps(10.0) .build()) .agentConfig(SessionExecuteParams.AgentConfig.builder() - .model(ModelConfig.ofModelConfigObject( - ModelConfig.ModelConfigObject.builder() - .modelName("openai/gpt-5-nano") - .apiKey(System.getenv("MODEL_API_KEY")) - .build() - )) + .model(ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey(System.getenv("MODEL_API_KEY")) + .build()) .cua(false) .build()) .build() @@ -555,6 +566,8 @@ If the SDK threw an exception, but you're _certain_ the version is compatible, t > [!CAUTION] > We make no guarantee that the SDK works correctly when the Jackson version check is disabled. +Also note that there are bugs in older Jackson versions that can affect the SDK. We don't work around all Jackson bugs ([example](https://github.com/FasterXML/jackson-databind/issues/3240)) and expect users to upgrade Jackson for those instead. + ## Network options ### Retries diff --git a/build.gradle.kts b/build.gradle.kts index 7c2c0c2..f240422 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,4 @@ plugins { - id("io.github.gradle-nexus.publish-plugin") version "1.1.0" id("org.jetbrains.dokka") version "2.0.0" } @@ -9,7 +8,7 @@ repositories { allprojects { group = "com.browserbase.api" - version = "0.6.1" // x-release-please-version + version = "0.7.0" // x-release-please-version } subprojects { @@ -35,15 +34,3 @@ tasks.named("dokkaJavadocCollector").configure { .filter { it.project.name != "stagehand-java" && it.name == "dokkaJavadocJar" } .forEach { mustRunAfter(it) } } - -nexusPublishing { - repositories { - sonatype { - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - - username.set(System.getenv("SONATYPE_USERNAME")) - password.set(System.getenv("SONATYPE_PASSWORD")) - } - } -} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 0b14135..c6dc92e 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,12 +1,15 @@ plugins { `kotlin-dsl` kotlin("jvm") version "1.9.20" + id("com.vanniktech.maven.publish") version "0.28.0" } repositories { gradlePluginPortal() + mavenCentral() } dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20") + implementation("com.vanniktech:gradle-maven-publish-plugin:0.28.0") } diff --git a/buildSrc/src/main/kotlin/stagehand.java.gradle.kts b/buildSrc/src/main/kotlin/stagehand.java.gradle.kts index 81d5d32..70fc33f 100644 --- a/buildSrc/src/main/kotlin/stagehand.java.gradle.kts +++ b/buildSrc/src/main/kotlin/stagehand.java.gradle.kts @@ -8,11 +8,6 @@ repositories { mavenCentral() } -configure { - withJavadocJar() - withSourcesJar() -} - java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) @@ -27,10 +22,6 @@ tasks.withType().configureEach { options.release.set(8) } -tasks.named("javadocJar") { - setZip64(true) -} - tasks.named("jar") { manifest { attributes(mapOf( diff --git a/buildSrc/src/main/kotlin/stagehand.kotlin.gradle.kts b/buildSrc/src/main/kotlin/stagehand.kotlin.gradle.kts index f3f48d1..be6cf65 100644 --- a/buildSrc/src/main/kotlin/stagehand.kotlin.gradle.kts +++ b/buildSrc/src/main/kotlin/stagehand.kotlin.gradle.kts @@ -33,6 +33,9 @@ kotlin { tasks.withType().configureEach { systemProperty("junit.jupiter.execution.parallel.enabled", true) systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent") + + // `SKIP_MOCK_TESTS` affects which tests run so it must be added as input for proper cache invalidation. + inputs.property("skipMockTests", System.getenv("SKIP_MOCK_TESTS")).optional(true) } val ktfmt by configurations.creating diff --git a/buildSrc/src/main/kotlin/stagehand.publish.gradle.kts b/buildSrc/src/main/kotlin/stagehand.publish.gradle.kts index facd8ed..f45a9d7 100644 --- a/buildSrc/src/main/kotlin/stagehand.publish.gradle.kts +++ b/buildSrc/src/main/kotlin/stagehand.publish.gradle.kts @@ -1,68 +1,71 @@ +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.KotlinJvm +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import com.vanniktech.maven.publish.SonatypeHost + plugins { - `maven-publish` - signing + id("com.vanniktech.maven.publish") +} + +publishing { + repositories { + if (project.hasProperty("publishLocal")) { + maven { + name = "LocalFileSystem" + url = uri("${rootProject.layout.buildDirectory.get()}/local-maven-repo") + } + } + } } -configure { - publications { - register("maven") { - from(components["java"]) +repositories { + gradlePluginPortal() + mavenCentral() +} - pom { - name.set("Stagehand API") - description.set("Stagehand SDK for AI browser automation [ALPHA]. This API allows clients to\nexecute browser automation tasks remotely on the Browserbase cloud.\n\nAll endpoints except /sessions/start require an active session ID. Responses are\nstreamed using Server-Sent Events (SSE) when the `x-stream-response: true`\nheader is provided.\n\nThis SDK is currently ALPHA software and is not production ready! Please try it\nand give us your feedback, stay tuned for upcoming release announcements!") - url.set("https://docs.stagehand.dev") +extra["signingInMemoryKey"] = System.getenv("GPG_SIGNING_KEY") +extra["signingInMemoryKeyId"] = System.getenv("GPG_SIGNING_KEY_ID") +extra["signingInMemoryKeyPassword"] = System.getenv("GPG_SIGNING_PASSWORD") - licenses { - license { - name.set("Apache-2.0") - } - } +configure { + if (!project.hasProperty("publishLocal")) { + signAllPublications() + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + } - developers { - developer { - name.set("Stagehand") - } - } + coordinates(project.group.toString(), project.name, project.version.toString()) + configure( + KotlinJvm( + javadocJar = JavadocJar.Dokka("dokkaJavadoc"), + sourcesJar = true, + ) + ) - scm { - connection.set("scm:git:git://github.com/browserbase/stagehand-java.git") - developerConnection.set("scm:git:git://github.com/browserbase/stagehand-java.git") - url.set("https://github.com/browserbase/stagehand-java") - } + pom { + name.set("Stagehand API") + description.set("Stagehand SDK for AI browser automation [ALPHA]. This API allows clients to\nexecute browser automation tasks remotely on the Browserbase cloud.\n\nAll endpoints except /sessions/start require an active session ID. Responses are\nstreamed using Server-Sent Events (SSE) when the `x-stream-response: true`\nheader is provided.\n\nThis SDK is currently ALPHA software and is not production ready! Please try it\nand give us your feedback, stay tuned for upcoming release announcements!") + url.set("https://docs.stagehand.dev") - versionMapping { - allVariants { - fromResolutionResult() - } - } + licenses { + license { + name.set("MIT") } } - } - repositories { - if (project.hasProperty("publishLocal")) { - maven { - name = "LocalFileSystem" - url = uri("${rootProject.layout.buildDirectory.get()}/local-maven-repo") + + developers { + developer { + name.set("Stagehand") } } - } -} -signing { - val signingKeyId = System.getenv("GPG_SIGNING_KEY_ID")?.ifBlank { null } - val signingKey = System.getenv("GPG_SIGNING_KEY")?.ifBlank { null } - val signingPassword = System.getenv("GPG_SIGNING_PASSWORD")?.ifBlank { null } - if (signingKey != null && signingPassword != null) { - useInMemoryPgpKeys( - signingKeyId, - signingKey, - signingPassword, - ) - sign(publishing.publications["maven"]) + scm { + connection.set("scm:git:git://github.com/browserbase/stagehand-java.git") + developerConnection.set("scm:git:git://github.com/browserbase/stagehand-java.git") + url.set("https://github.com/browserbase/stagehand-java") + } } } -tasks.named("publish") { - dependsOn(":closeAndReleaseSonatypeStagingRepository") +tasks.withType().configureEach { + isZip64 = true } diff --git a/scripts/build b/scripts/build index f406348..16a2b00 100755 --- a/scripts/build +++ b/scripts/build @@ -5,4 +5,4 @@ set -e cd "$(dirname "$0")/.." echo "==> Building classes" -./gradlew build testClasses -x test +./gradlew build testClasses "$@" -x test diff --git a/scripts/upload-artifacts b/scripts/upload-artifacts index 729e6f2..10f3c70 100755 --- a/scripts/upload-artifacts +++ b/scripts/upload-artifacts @@ -7,6 +7,8 @@ GREEN='\033[32m' RED='\033[31m' NC='\033[0m' # No Color +MAVEN_REPO_PATH="./build/local-maven-repo" + log_error() { local msg="$1" local headers="$2" @@ -24,7 +26,7 @@ upload_file() { if [ -f "$file_name" ]; then echo -e "${GREEN}Processing file: $file_name${NC}" - pkg_file_name="mvn${file_name#./build/local-maven-repo}" + pkg_file_name="mvn${file_name#"${MAVEN_REPO_PATH}"}" # Get signed URL for uploading artifact file signed_url_response=$(curl -X POST -G "$URL" \ @@ -47,18 +49,20 @@ upload_file() { md5|sha1|sha256|sha512) content_type="text/plain" ;; module) content_type="application/json" ;; pom|xml) content_type="application/xml" ;; + html) content_type="text/html" ;; *) content_type="application/octet-stream" ;; esac # Upload file upload_response=$(curl -v -X PUT \ --retry 5 \ + --retry-all-errors \ -D "$tmp_headers" \ -H "Content-Type: $content_type" \ --data-binary "@${file_name}" "$signed_url" 2>&1) if ! echo "$upload_response" | grep -q "HTTP/[0-9.]* 200"; then - log_error "Failed upload artifact file" "$tmp_headers" "$upload_response" + log_error "Failed to upload artifact file" "$tmp_headers" "$upload_response" fi # Insert small throttle to reduce rate limiting risk @@ -81,6 +85,99 @@ walk_tree() { done } +generate_instructions() { + cat << EOF > "$MAVEN_REPO_PATH/index.html" + + + + Maven Repo + + +

Stainless SDK Maven Repository

+

This is the Maven repository for your Stainless Java SDK build.

+ +

Project configuration

+ +

The details depend on whether you're using Maven or Gradle as your build tool.

+ +

Maven

+ +

Add the following to your project's pom.xml:

+
<repositories>
+    <repository>
+        <id>stainless-sdk-repo</id>
+        <url>https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn</url>
+    </repository>
+</repositories>
+ +

Gradle

+

Add the following to your build.gradle file:

+
repositories {
+    maven {
+        url "https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn"
+    }
+}
+ +
+

Configuring authentication (if required)

+ +

Some accounts may require authentication to access the repository. If so, use the + following instructions, replacing YOUR_STAINLESS_API_TOKEN with your actual token.

+ +

Maven with authentication

+ +

First, ensure you have the following in your Maven settings.xml for repo authentication:

+
<servers>
+    <server>
+        <id>stainless-sdk-repo</id>
+        <configuration>
+            <httpHeaders>
+                <property>
+                    <name>Authorization</name>
+                    <value>Bearer YOUR_STAINLESS_API_TOKEN</value>
+                </property>
+            </httpHeaders>
+        </configuration>
+    </server>
+</servers>
+ +

Then, add the following to your project's pom.xml:

+
<repositories>
+    <repository>
+        <id>stainless-sdk-repo</id>
+        <url>https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn</url>
+    </repository>
+</repositories>
+ +

Gradle with authentication

+

Add the following to your build.gradle file:

+
repositories {
+    maven {
+        url "https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn"
+        credentials(HttpHeaderCredentials) {
+            name = "Authorization"
+            value = "Bearer YOUR_STAINLESS_API_TOKEN"
+        }
+        authentication {
+            header(HttpHeaderAuthentication)
+        }
+    }
+}
+
+ +

Using the repository

+

Once you've configured the repository, you can include dependencies from it as usual. See your + project README + for more details.

+ + +EOF + upload_file "${MAVEN_REPO_PATH}/index.html" + + echo "Configure maven or gradle to use the repo located at 'https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn'" + echo "For more details, see the directions in https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn/index.html" +} + cd "$(dirname "$0")/.." echo "::group::Creating local Maven content" @@ -88,9 +185,9 @@ echo "::group::Creating local Maven content" echo "::endgroup::" echo "::group::Uploading to pkg.stainless.com" -walk_tree "./build/local-maven-repo" +walk_tree "$MAVEN_REPO_PATH" echo "::endgroup::" echo "::group::Generating instructions" -echo "Configure maven or gradle to use the repo located at 'https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn'" +generate_instructions echo "::endgroup::" diff --git a/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/OkHttpClient.kt b/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/OkHttpClient.kt index 116714c..dadd500 100644 --- a/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/OkHttpClient.kt +++ b/stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/OkHttpClient.kt @@ -234,6 +234,8 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien fun build(): OkHttpClient = OkHttpClient( okhttp3.OkHttpClient.Builder() + // `RetryingHttpClient` handles retries if the user enabled them. + .retryOnConnectionFailure(false) .connectTimeout(timeout.connect()) .readTimeout(timeout.read()) .writeTimeout(timeout.write()) diff --git a/stagehand-java-core/build.gradle.kts b/stagehand-java-core/build.gradle.kts index 38bcf47..3cfe35b 100644 --- a/stagehand-java-core/build.gradle.kts +++ b/stagehand-java-core/build.gradle.kts @@ -5,14 +5,16 @@ plugins { configurations.all { resolutionStrategy { - // Compile and test against a lower Jackson version to ensure we're compatible with it. - // We publish with a higher version (see below) to ensure users depend on a secure version by default. - force("com.fasterxml.jackson.core:jackson-core:2.13.4") - force("com.fasterxml.jackson.core:jackson-databind:2.13.4") - force("com.fasterxml.jackson.core:jackson-annotations:2.13.4") - force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4") - force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4") - force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4") + // Compile and test against a lower Jackson version to ensure we're compatible with it. Note that + // we generally support 2.13.4, but test against 2.14.0 because 2.13.4 has some annoying (but + // niche) bugs (users should upgrade if they encounter them). We publish with a higher version + // (see below) to ensure users depend on a secure version by default. + force("com.fasterxml.jackson.core:jackson-core:2.14.0") + force("com.fasterxml.jackson.core:jackson-databind:2.14.0") + force("com.fasterxml.jackson.core:jackson-annotations:2.14.0") + force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.0") + force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.0") + force("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0") } } 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 732c468..8b1c7f0 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 @@ -484,6 +484,7 @@ private constructor( headers.put("X-Stainless-Package-Version", getPackageVersion()) headers.put("X-Stainless-Runtime", "JRE") headers.put("X-Stainless-Runtime-Version", getJavaVersion()) + headers.put("X-Stainless-Kotlin-Version", KotlinVersion.CURRENT.toString()) browserbaseApiKey.let { if (!it.isEmpty()) { headers.put("x-bb-api-key", it) diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ObjectMappers.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ObjectMappers.kt index 9ed574e..5200d47 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ObjectMappers.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ObjectMappers.kt @@ -24,7 +24,8 @@ import java.io.InputStream import java.time.DateTimeException import java.time.LocalDate import java.time.LocalDateTime -import java.time.ZonedDateTime +import java.time.OffsetDateTime +import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.temporal.ChronoField @@ -36,7 +37,7 @@ fun jsonMapper(): JsonMapper = .addModule( SimpleModule() .addSerializer(InputStreamSerializer) - .addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer()) + .addDeserializer(OffsetDateTime::class.java, LenientOffsetDateTimeDeserializer()) ) .withCoercionConfig(LogicalType.Boolean) { it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) @@ -47,6 +48,7 @@ fun jsonMapper(): JsonMapper = } .withCoercionConfig(LogicalType.Integer) { it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) .setCoercion(CoercionInputShape.String, CoercionAction.Fail) .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) @@ -64,6 +66,12 @@ fun jsonMapper(): JsonMapper = .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) } + .withCoercionConfig(LogicalType.DateTime) { + it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) + } .withCoercionConfig(LogicalType.Array) { it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail) .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) @@ -124,10 +132,10 @@ private object InputStreamSerializer : BaseSerializer(InputStream:: } /** - * A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes. + * A deserializer that can deserialize [OffsetDateTime] from datetimes, dates, and zoned datetimes. */ -private class LenientLocalDateTimeDeserializer : - StdDeserializer(LocalDateTime::class.java) { +private class LenientOffsetDateTimeDeserializer : + StdDeserializer(OffsetDateTime::class.java) { companion object { @@ -141,7 +149,7 @@ private class LenientLocalDateTimeDeserializer : override fun logicalType(): LogicalType = LogicalType.DateTime - override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime { + override fun deserialize(p: JsonParser, context: DeserializationContext): OffsetDateTime { val exceptions = mutableListOf() for (formatter in DATE_TIME_FORMATTERS) { @@ -150,17 +158,20 @@ private class LenientLocalDateTimeDeserializer : return when { !temporal.isSupported(ChronoField.HOUR_OF_DAY) -> - LocalDate.from(temporal).atStartOfDay() + LocalDate.from(temporal) + .atStartOfDay() + .atZone(ZoneId.of("UTC")) + .toOffsetDateTime() !temporal.isSupported(ChronoField.OFFSET_SECONDS) -> - LocalDateTime.from(temporal) - else -> ZonedDateTime.from(temporal).toLocalDateTime() + LocalDateTime.from(temporal).atZone(ZoneId.of("UTC")).toOffsetDateTime() + else -> OffsetDateTime.from(temporal) } } catch (e: DateTimeException) { exceptions.add(e) } } - throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply { + throw JsonParseException(p, "Cannot parse `OffsetDateTime` from value: ${p.text}").apply { exceptions.forEach { addSuppressed(it) } } } 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 039faae..f8e870e 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 @@ -28,7 +28,7 @@ internal fun sseHandler(jsonMapper: JsonMapper): Handler { + 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 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 7e1a7aa..1305c6a 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 @@ -31,10 +31,6 @@ private constructor( ) : HttpClient { override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse { - if (!isRetryable(request) || maxRetries <= 0) { - return httpClient.execute(request, requestOptions) - } - var modifiedRequest = maybeAddIdempotencyHeader(request) // Don't send the current retry count in the headers if the caller set their own value. @@ -48,6 +44,10 @@ private constructor( modifiedRequest = setRetryCountHeader(modifiedRequest, retries) } + if (!isRetryable(modifiedRequest)) { + return httpClient.execute(modifiedRequest, requestOptions) + } + val response = try { val response = httpClient.execute(modifiedRequest, requestOptions) @@ -75,10 +75,6 @@ private constructor( request: HttpRequest, requestOptions: RequestOptions, ): CompletableFuture { - if (!isRetryable(request) || maxRetries <= 0) { - return httpClient.executeAsync(request, requestOptions) - } - val modifiedRequest = maybeAddIdempotencyHeader(request) // Don't send the current retry count in the headers if the caller set their own value. @@ -94,8 +90,12 @@ private constructor( val requestWithRetryCount = if (shouldSendRetryCount) setRetryCountHeader(request, retries) else request - return httpClient - .executeAsync(requestWithRetryCount, requestOptions) + val responseFuture = httpClient.executeAsync(requestWithRetryCount, requestOptions) + if (!isRetryable(requestWithRetryCount)) { + return responseFuture + } + + return responseFuture .handleAsync( fun( response: HttpResponse?, 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 ce0785b..49a3d65 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 @@ -2,449 +2,372 @@ package com.browserbase.api.models.sessions -import com.browserbase.api.core.BaseDeserializer -import com.browserbase.api.core.BaseSerializer import com.browserbase.api.core.Enum import com.browserbase.api.core.ExcludeMissing import com.browserbase.api.core.JsonField import com.browserbase.api.core.JsonMissing import com.browserbase.api.core.JsonValue -import com.browserbase.api.core.allMaxBy import com.browserbase.api.core.checkRequired -import com.browserbase.api.core.getOrThrow import com.browserbase.api.errors.StagehandInvalidDataException import com.fasterxml.jackson.annotation.JsonAnyGetter import com.fasterxml.jackson.annotation.JsonAnySetter import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.ObjectCodec -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonSerialize -import com.fasterxml.jackson.module.kotlin.jacksonTypeRef import java.util.Collections import java.util.Objects import java.util.Optional import kotlin.jvm.optionals.getOrNull -/** - * Model name string with provider prefix (e.g., 'openai/gpt-5-nano', 'anthropic/claude-4.5-opus') - */ -@JsonDeserialize(using = ModelConfig.Deserializer::class) -@JsonSerialize(using = ModelConfig.Serializer::class) class ModelConfig +@JsonCreator(mode = JsonCreator.Mode.DISABLED) private constructor( - private val name: String? = null, - private val modelConfigObject: ModelConfigObject? = null, - private val _json: JsonValue? = null, + private val modelName: JsonField, + private val apiKey: JsonField, + private val baseUrl: JsonField, + private val provider: JsonField, + private val additionalProperties: MutableMap, ) { + @JsonCreator + private constructor( + @JsonProperty("modelName") @ExcludeMissing modelName: JsonField = JsonMissing.of(), + @JsonProperty("apiKey") @ExcludeMissing apiKey: JsonField = JsonMissing.of(), + @JsonProperty("baseURL") @ExcludeMissing baseUrl: JsonField = JsonMissing.of(), + @JsonProperty("provider") @ExcludeMissing provider: JsonField = JsonMissing.of(), + ) : this(modelName, apiKey, baseUrl, provider, mutableMapOf()) + /** - * Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - * 'anthropic/claude-4.5-opus') + * Model name string with provider prefix (e.g., 'openai/gpt-5-nano') + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). */ - fun name(): Optional = Optional.ofNullable(name) - - fun modelConfigObject(): Optional = Optional.ofNullable(modelConfigObject) - - fun isName(): Boolean = name != null - - fun isModelConfigObject(): Boolean = modelConfigObject != null + fun modelName(): String = modelName.getRequired("modelName") /** - * Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - * 'anthropic/claude-4.5-opus') + * API key for the model provider + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). */ - fun asName(): String = name.getOrThrow("name") - - fun asModelConfigObject(): ModelConfigObject = modelConfigObject.getOrThrow("modelConfigObject") - - fun _json(): Optional = Optional.ofNullable(_json) + fun apiKey(): Optional = apiKey.getOptional("apiKey") - fun accept(visitor: Visitor): T = - when { - name != null -> visitor.visitName(name) - modelConfigObject != null -> visitor.visitModelConfigObject(modelConfigObject) - else -> visitor.unknown(_json) - } - - private var validated: Boolean = false + /** + * Base URL for the model provider + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun baseUrl(): Optional = baseUrl.getOptional("baseURL") - fun validate(): ModelConfig = apply { - if (validated) { - return@apply - } + /** + * AI provider for the model (or provide a baseURL endpoint instead) + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun provider(): Optional = provider.getOptional("provider") - accept( - object : Visitor { - override fun visitName(name: String) {} + /** + * Returns the raw JSON value of [modelName]. + * + * Unlike [modelName], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("modelName") @ExcludeMissing fun _modelName(): JsonField = modelName - override fun visitModelConfigObject(modelConfigObject: ModelConfigObject) { - modelConfigObject.validate() - } - } - ) - validated = true - } + /** + * Returns the raw JSON value of [apiKey]. + * + * Unlike [apiKey], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("apiKey") @ExcludeMissing fun _apiKey(): JsonField = apiKey - fun isValid(): Boolean = - try { - validate() - true - } catch (e: StagehandInvalidDataException) { - false - } + /** + * Returns the raw JSON value of [baseUrl]. + * + * Unlike [baseUrl], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("baseURL") @ExcludeMissing fun _baseUrl(): JsonField = baseUrl /** - * Returns a score indicating how many valid values are contained in this object recursively. + * Returns the raw JSON value of [provider]. * - * Used for best match union deserialization. + * Unlike [provider], this method doesn't throw if the JSON field has an unexpected type. */ - @JvmSynthetic - internal fun validity(): Int = - accept( - object : Visitor { - override fun visitName(name: String) = 1 + @JsonProperty("provider") @ExcludeMissing fun _provider(): JsonField = provider - override fun visitModelConfigObject(modelConfigObject: ModelConfigObject) = - modelConfigObject.validity() + @JsonAnySetter + private fun putAdditionalProperty(key: String, value: JsonValue) { + additionalProperties.put(key, value) + } - override fun unknown(json: JsonValue?) = 0 - } - ) + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = + Collections.unmodifiableMap(additionalProperties) - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } + fun toBuilder() = Builder().from(this) - return other is ModelConfig && - name == other.name && - modelConfigObject == other.modelConfigObject + companion object { + + /** + * Returns a mutable builder for constructing an instance of [ModelConfig]. + * + * The following fields are required: + * ```java + * .modelName() + * ``` + */ + @JvmStatic fun builder() = Builder() } - override fun hashCode(): Int = Objects.hash(name, modelConfigObject) + /** A builder for [ModelConfig]. */ + class Builder internal constructor() { + + private var modelName: JsonField? = null + private var apiKey: JsonField = JsonMissing.of() + private var baseUrl: JsonField = JsonMissing.of() + private var provider: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() - override fun toString(): String = - when { - name != null -> "ModelConfig{name=$name}" - modelConfigObject != null -> "ModelConfig{modelConfigObject=$modelConfigObject}" - _json != null -> "ModelConfig{_unknown=$_json}" - else -> throw IllegalStateException("Invalid ModelConfig") + @JvmSynthetic + internal fun from(modelConfig: ModelConfig) = apply { + modelName = modelConfig.modelName + apiKey = modelConfig.apiKey + baseUrl = modelConfig.baseUrl + provider = modelConfig.provider + additionalProperties = modelConfig.additionalProperties.toMutableMap() } - companion object { + /** Model name string with provider prefix (e.g., 'openai/gpt-5-nano') */ + fun modelName(modelName: String) = modelName(JsonField.of(modelName)) /** - * Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - * 'anthropic/claude-4.5-opus') + * Sets [Builder.modelName] to an arbitrary JSON value. + * + * You should usually call [Builder.modelName] with a well-typed [String] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. */ - @JvmStatic fun ofName(name: String) = ModelConfig(name = name) + fun modelName(modelName: JsonField) = apply { this.modelName = modelName } - @JvmStatic - fun ofModelConfigObject(modelConfigObject: ModelConfigObject) = - ModelConfig(modelConfigObject = modelConfigObject) - } - - /** - * An interface that defines how to map each variant of [ModelConfig] to a value of type [T]. - */ - interface Visitor { + /** API key for the model provider */ + fun apiKey(apiKey: String) = apiKey(JsonField.of(apiKey)) /** - * Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - * 'anthropic/claude-4.5-opus') + * Sets [Builder.apiKey] to an arbitrary JSON value. + * + * You should usually call [Builder.apiKey] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. */ - fun visitName(name: String): T + fun apiKey(apiKey: JsonField) = apply { this.apiKey = apiKey } - fun visitModelConfigObject(modelConfigObject: ModelConfigObject): T + /** Base URL for the model provider */ + fun baseUrl(baseUrl: String) = baseUrl(JsonField.of(baseUrl)) /** - * Maps an unknown variant of [ModelConfig] to a value of type [T]. + * Sets [Builder.baseUrl] to an arbitrary JSON value. * - * An instance of [ModelConfig] can contain an unknown variant if it was deserialized from - * data that doesn't match any known variant. For example, if the SDK is on an older version - * than the API, then the API may respond with new variants that the SDK is unaware of. + * You should usually call [Builder.baseUrl] with a well-typed [String] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun baseUrl(baseUrl: JsonField) = apply { this.baseUrl = baseUrl } + + /** AI provider for the model (or provide a baseURL endpoint instead) */ + fun provider(provider: Provider) = provider(JsonField.of(provider)) + + /** + * Sets [Builder.provider] to an arbitrary JSON value. * - * @throws StagehandInvalidDataException in the default implementation. + * You should usually call [Builder.provider] with a well-typed [Provider] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. */ - fun unknown(json: JsonValue?): T { - throw StagehandInvalidDataException("Unknown ModelConfig: $json") + fun provider(provider: JsonField) = apply { this.provider = provider } + + fun additionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.clear() + putAllAdditionalProperties(additionalProperties) } - } - internal class Deserializer : BaseDeserializer(ModelConfig::class) { - - override fun ObjectCodec.deserialize(node: JsonNode): ModelConfig { - val json = JsonValue.fromJsonNode(node) - - val bestMatches = - sequenceOf( - tryDeserialize(node, jacksonTypeRef())?.let { - ModelConfig(modelConfigObject = it, _json = json) - }, - tryDeserialize(node, jacksonTypeRef())?.let { - ModelConfig(name = it, _json = json) - }, - ) - .filterNotNull() - .allMaxBy { it.validity() } - .toList() - return when (bestMatches.size) { - // This can happen if what we're deserializing is completely incompatible with all - // the possible variants (e.g. deserializing from array). - 0 -> ModelConfig(_json = json) - 1 -> bestMatches.single() - // If there's more than one match with the highest validity, then use the first - // completely valid match, or simply the first match if none are completely valid. - else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() - } + fun putAdditionalProperty(key: String, value: JsonValue) = apply { + additionalProperties.put(key, value) } - } - internal class Serializer : BaseSerializer(ModelConfig::class) { - - override fun serialize( - value: ModelConfig, - generator: JsonGenerator, - provider: SerializerProvider, - ) { - when { - value.name != null -> generator.writeObject(value.name) - value.modelConfigObject != null -> generator.writeObject(value.modelConfigObject) - value._json != null -> generator.writeObject(value._json) - else -> throw IllegalStateException("Invalid ModelConfig") - } + fun putAllAdditionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.putAll(additionalProperties) } - } - class ModelConfigObject - @JsonCreator(mode = JsonCreator.Mode.DISABLED) - private constructor( - private val modelName: JsonField, - private val apiKey: JsonField, - private val baseUrl: JsonField, - private val provider: JsonField, - private val additionalProperties: MutableMap, - ) { - - @JsonCreator - private constructor( - @JsonProperty("modelName") - @ExcludeMissing - modelName: JsonField = JsonMissing.of(), - @JsonProperty("apiKey") @ExcludeMissing apiKey: JsonField = JsonMissing.of(), - @JsonProperty("baseURL") @ExcludeMissing baseUrl: JsonField = JsonMissing.of(), - @JsonProperty("provider") - @ExcludeMissing - provider: JsonField = JsonMissing.of(), - ) : this(modelName, apiKey, baseUrl, provider, mutableMapOf()) + fun removeAdditionalProperty(key: String) = apply { additionalProperties.remove(key) } - /** - * Model name string (e.g., 'openai/gpt-5-nano', 'anthropic/claude-4.5-opus') - * - * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is - * unexpectedly missing or null (e.g. if the server responded with an unexpected value). - */ - fun modelName(): String = modelName.getRequired("modelName") + fun removeAllAdditionalProperties(keys: Set) = apply { + keys.forEach(::removeAdditionalProperty) + } /** - * API key for the model provider + * Returns an immutable instance of [ModelConfig]. * - * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if - * the server responded with an unexpected value). - */ - fun apiKey(): Optional = apiKey.getOptional("apiKey") - - /** - * Base URL for the model provider + * Further updates to this [Builder] will not mutate the returned instance. * - * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if - * the server responded with an unexpected value). - */ - fun baseUrl(): Optional = baseUrl.getOptional("baseURL") - - /** - * AI provider for the model (or provide a baseURL endpoint instead) + * The following fields are required: + * ```java + * .modelName() + * ``` * - * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if - * the server responded with an unexpected value). + * @throws IllegalStateException if any required field is unset. */ - fun provider(): Optional = provider.getOptional("provider") + fun build(): ModelConfig = + ModelConfig( + checkRequired("modelName", modelName), + apiKey, + baseUrl, + provider, + additionalProperties.toMutableMap(), + ) + } - /** - * Returns the raw JSON value of [modelName]. - * - * Unlike [modelName], this method doesn't throw if the JSON field has an unexpected type. - */ - @JsonProperty("modelName") @ExcludeMissing fun _modelName(): JsonField = modelName + private var validated: Boolean = false - /** - * Returns the raw JSON value of [apiKey]. - * - * Unlike [apiKey], this method doesn't throw if the JSON field has an unexpected type. - */ - @JsonProperty("apiKey") @ExcludeMissing fun _apiKey(): JsonField = apiKey + fun validate(): ModelConfig = apply { + if (validated) { + return@apply + } - /** - * Returns the raw JSON value of [baseUrl]. - * - * Unlike [baseUrl], this method doesn't throw if the JSON field has an unexpected type. - */ - @JsonProperty("baseURL") @ExcludeMissing fun _baseUrl(): JsonField = baseUrl + modelName() + apiKey() + baseUrl() + provider().ifPresent { it.validate() } + 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 = + (if (modelName.asKnown().isPresent) 1 else 0) + + (if (apiKey.asKnown().isPresent) 1 else 0) + + (if (baseUrl.asKnown().isPresent) 1 else 0) + + (provider.asKnown().getOrNull()?.validity() ?: 0) + + /** AI provider for the model (or provide a baseURL endpoint instead) */ + class Provider @JsonCreator private constructor(private val value: JsonField) : Enum { /** - * Returns the raw JSON value of [provider]. + * Returns this class instance's raw value. * - * Unlike [provider], this method doesn't throw if the JSON field has an unexpected type. + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. */ - @JsonProperty("provider") @ExcludeMissing fun _provider(): JsonField = provider + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value - @JsonAnySetter - private fun putAdditionalProperty(key: String, value: JsonValue) { - additionalProperties.put(key, value) - } + companion object { - @JsonAnyGetter - @ExcludeMissing - fun _additionalProperties(): Map = - Collections.unmodifiableMap(additionalProperties) + @JvmField val OPENAI = of("openai") - fun toBuilder() = Builder().from(this) + @JvmField val ANTHROPIC = of("anthropic") - companion object { + @JvmField val GOOGLE = of("google") + + @JvmField val MICROSOFT = of("microsoft") - /** - * Returns a mutable builder for constructing an instance of [ModelConfigObject]. - * - * The following fields are required: - * ```java - * .modelName() - * ``` - */ - @JvmStatic fun builder() = Builder() + @JvmStatic fun of(value: String) = Provider(JsonField.of(value)) } - /** A builder for [ModelConfigObject]. */ - class Builder internal constructor() { - - private var modelName: JsonField? = null - private var apiKey: JsonField = JsonMissing.of() - private var baseUrl: JsonField = JsonMissing.of() - private var provider: JsonField = JsonMissing.of() - private var additionalProperties: MutableMap = mutableMapOf() - - @JvmSynthetic - internal fun from(modelConfigObject: ModelConfigObject) = apply { - modelName = modelConfigObject.modelName - apiKey = modelConfigObject.apiKey - baseUrl = modelConfigObject.baseUrl - provider = modelConfigObject.provider - additionalProperties = modelConfigObject.additionalProperties.toMutableMap() - } + /** An enum containing [Provider]'s known values. */ + enum class Known { + OPENAI, + ANTHROPIC, + GOOGLE, + MICROSOFT, + } - /** Model name string (e.g., 'openai/gpt-5-nano', 'anthropic/claude-4.5-opus') */ - fun modelName(modelName: String) = modelName(JsonField.of(modelName)) - - /** - * Sets [Builder.modelName] to an arbitrary JSON value. - * - * You should usually call [Builder.modelName] with a well-typed [String] value instead. - * This method is primarily for setting the field to an undocumented or not yet - * supported value. - */ - fun modelName(modelName: JsonField) = apply { this.modelName = modelName } - - /** API key for the model provider */ - fun apiKey(apiKey: String) = apiKey(JsonField.of(apiKey)) - - /** - * Sets [Builder.apiKey] to an arbitrary JSON value. - * - * You should usually call [Builder.apiKey] with a well-typed [String] value instead. - * This method is primarily for setting the field to an undocumented or not yet - * supported value. - */ - fun apiKey(apiKey: JsonField) = apply { this.apiKey = apiKey } - - /** Base URL for the model provider */ - fun baseUrl(baseUrl: String) = baseUrl(JsonField.of(baseUrl)) - - /** - * Sets [Builder.baseUrl] to an arbitrary JSON value. - * - * You should usually call [Builder.baseUrl] with a well-typed [String] value instead. - * This method is primarily for setting the field to an undocumented or not yet - * supported value. - */ - fun baseUrl(baseUrl: JsonField) = apply { this.baseUrl = baseUrl } - - /** AI provider for the model (or provide a baseURL endpoint instead) */ - fun provider(provider: Provider) = provider(JsonField.of(provider)) - - /** - * Sets [Builder.provider] to an arbitrary JSON value. - * - * You should usually call [Builder.provider] with a well-typed [Provider] value - * instead. This method is primarily for setting the field to an undocumented or not yet - * supported value. - */ - fun provider(provider: JsonField) = apply { this.provider = provider } - - fun additionalProperties(additionalProperties: Map) = apply { - this.additionalProperties.clear() - putAllAdditionalProperties(additionalProperties) - } + /** + * An enum containing [Provider]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Provider] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + OPENAI, + ANTHROPIC, + GOOGLE, + MICROSOFT, + /** An enum member indicating that [Provider] was instantiated with an unknown value. */ + _UNKNOWN, + } - fun putAdditionalProperty(key: String, value: JsonValue) = apply { - additionalProperties.put(key, value) + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + OPENAI -> Value.OPENAI + ANTHROPIC -> Value.ANTHROPIC + GOOGLE -> Value.GOOGLE + MICROSOFT -> Value.MICROSOFT + else -> Value._UNKNOWN } - fun putAllAdditionalProperties(additionalProperties: Map) = apply { - this.additionalProperties.putAll(additionalProperties) + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + OPENAI -> Known.OPENAI + ANTHROPIC -> Known.ANTHROPIC + GOOGLE -> Known.GOOGLE + MICROSOFT -> Known.MICROSOFT + else -> throw StagehandInvalidDataException("Unknown Provider: $value") } - fun removeAdditionalProperty(key: String) = apply { additionalProperties.remove(key) } - - fun removeAllAdditionalProperties(keys: Set) = apply { - keys.forEach(::removeAdditionalProperty) + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") } - /** - * Returns an immutable instance of [ModelConfigObject]. - * - * Further updates to this [Builder] will not mutate the returned instance. - * - * The following fields are required: - * ```java - * .modelName() - * ``` - * - * @throws IllegalStateException if any required field is unset. - */ - fun build(): ModelConfigObject = - ModelConfigObject( - checkRequired("modelName", modelName), - apiKey, - baseUrl, - provider, - additionalProperties.toMutableMap(), - ) - } - private var validated: Boolean = false - fun validate(): ModelConfigObject = apply { + fun validate(): Provider = apply { if (validated) { return@apply } - modelName() - apiKey() - baseUrl() - provider().ifPresent { it.validate() } + known() validated = true } @@ -462,176 +385,40 @@ private constructor( * * Used for best match union deserialization. */ - @JvmSynthetic - internal fun validity(): Int = - (if (modelName.asKnown().isPresent) 1 else 0) + - (if (apiKey.asKnown().isPresent) 1 else 0) + - (if (baseUrl.asKnown().isPresent) 1 else 0) + - (provider.asKnown().getOrNull()?.validity() ?: 0) - - /** AI provider for the model (or provide a baseURL endpoint instead) */ - class Provider @JsonCreator private constructor(private val value: JsonField) : - Enum { - - /** - * Returns this class instance's raw value. - * - * This is usually only useful if this instance was deserialized from data that doesn't - * match any known member, and you want to know that value. For example, if the SDK is - * on an older version than the API, then the API may respond with new members that the - * SDK is unaware of. - */ - @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value - - companion object { - - @JvmField val OPENAI = of("openai") - - @JvmField val ANTHROPIC = of("anthropic") - - @JvmField val GOOGLE = of("google") - - @JvmField val MICROSOFT = of("microsoft") - - @JvmStatic fun of(value: String) = Provider(JsonField.of(value)) - } - - /** An enum containing [Provider]'s known values. */ - enum class Known { - OPENAI, - ANTHROPIC, - GOOGLE, - MICROSOFT, - } - - /** - * An enum containing [Provider]'s known values, as well as an [_UNKNOWN] member. - * - * An instance of [Provider] can contain an unknown value in a couple of cases: - * - It was deserialized from data that doesn't match any known member. For example, if - * the SDK is on an older version than the API, then the API may respond with new - * members that the SDK is unaware of. - * - It was constructed with an arbitrary value using the [of] method. - */ - enum class Value { - OPENAI, - ANTHROPIC, - GOOGLE, - MICROSOFT, - /** - * An enum member indicating that [Provider] was instantiated with an unknown value. - */ - _UNKNOWN, - } - - /** - * Returns an enum member corresponding to this class instance's value, or - * [Value._UNKNOWN] if the class was instantiated with an unknown value. - * - * Use the [known] method instead if you're certain the value is always known or if you - * want to throw for the unknown case. - */ - fun value(): Value = - when (this) { - OPENAI -> Value.OPENAI - ANTHROPIC -> Value.ANTHROPIC - GOOGLE -> Value.GOOGLE - MICROSOFT -> Value.MICROSOFT - else -> Value._UNKNOWN - } - - /** - * Returns an enum member corresponding to this class instance's value. - * - * Use the [value] method instead if you're uncertain the value is always known and - * don't want to throw for the unknown case. - * - * @throws StagehandInvalidDataException if this class instance's value is a not a known - * member. - */ - fun known(): Known = - when (this) { - OPENAI -> Known.OPENAI - ANTHROPIC -> Known.ANTHROPIC - GOOGLE -> Known.GOOGLE - MICROSOFT -> Known.MICROSOFT - else -> throw StagehandInvalidDataException("Unknown Provider: $value") - } - - /** - * Returns this class instance's primitive wire representation. - * - * This differs from the [toString] method because that method is primarily for - * debugging and generally doesn't throw. - * - * @throws StagehandInvalidDataException if this class instance's value does not have - * the expected primitive type. - */ - fun asString(): String = - _value().asString().orElseThrow { - StagehandInvalidDataException("Value is not a String") - } - - private var validated: Boolean = false - - fun validate(): Provider = apply { - if (validated) { - return@apply - } - - known() - 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 = if (value() == Value._UNKNOWN) 0 else 1 - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - - return other is Provider && value == other.value - } - - override fun hashCode() = value.hashCode() - - override fun toString() = value.toString() - } + @JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1 override fun equals(other: Any?): Boolean { if (this === other) { return true } - return other is ModelConfigObject && - modelName == other.modelName && - apiKey == other.apiKey && - baseUrl == other.baseUrl && - provider == other.provider && - additionalProperties == other.additionalProperties + return other is Provider && value == other.value } - private val hashCode: Int by lazy { - Objects.hash(modelName, apiKey, baseUrl, provider, additionalProperties) + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true } - override fun hashCode(): Int = hashCode + return other is ModelConfig && + modelName == other.modelName && + apiKey == other.apiKey && + baseUrl == other.baseUrl && + provider == other.provider && + additionalProperties == other.additionalProperties + } - override fun toString() = - "ModelConfigObject{modelName=$modelName, apiKey=$apiKey, baseUrl=$baseUrl, provider=$provider, additionalProperties=$additionalProperties}" + private val hashCode: Int by lazy { + Objects.hash(modelName, apiKey, baseUrl, provider, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "ModelConfig{modelName=$modelName, apiKey=$apiKey, baseUrl=$baseUrl, 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 182931c..3445d78 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 @@ -28,8 +28,6 @@ import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.fasterxml.jackson.module.kotlin.jacksonTypeRef -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter import java.util.Collections import java.util.Objects import java.util.Optional @@ -39,7 +37,6 @@ import kotlin.jvm.optionals.getOrNull class SessionActParams private constructor( private val id: String?, - private val xSentAt: OffsetDateTime?, private val xStreamResponse: XStreamResponse?, private val body: Body, private val additionalHeaders: Headers, @@ -49,9 +46,6 @@ private constructor( /** Unique session identifier */ fun id(): Optional = Optional.ofNullable(id) - /** ISO timestamp when request was sent */ - fun xSentAt(): Optional = Optional.ofNullable(xSentAt) - /** Whether to stream the response via SSE */ fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) @@ -125,7 +119,6 @@ private constructor( class Builder internal constructor() { private var id: String? = null - private var xSentAt: OffsetDateTime? = null private var xStreamResponse: XStreamResponse? = null private var body: Body.Builder = Body.builder() private var additionalHeaders: Headers.Builder = Headers.builder() @@ -134,7 +127,6 @@ private constructor( @JvmSynthetic internal fun from(sessionActParams: SessionActParams) = apply { id = sessionActParams.id - xSentAt = sessionActParams.xSentAt xStreamResponse = sessionActParams.xStreamResponse body = sessionActParams.body.toBuilder() additionalHeaders = sessionActParams.additionalHeaders.toBuilder() @@ -147,12 +139,6 @@ private constructor( /** Alias for calling [Builder.id] with `id.orElse(null)`. */ fun id(id: Optional) = id(id.getOrNull()) - /** ISO timestamp when request was sent */ - fun xSentAt(xSentAt: OffsetDateTime?) = apply { this.xSentAt = xSentAt } - - /** Alias for calling [Builder.xSentAt] with `xSentAt.orElse(null)`. */ - fun xSentAt(xSentAt: Optional) = xSentAt(xSentAt.getOrNull()) - /** Whether to stream the response via SSE */ fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { this.xStreamResponse = xStreamResponse @@ -191,7 +177,10 @@ private constructor( fun input(action: Action) = apply { body.input(action) } /** Target frame ID for the action */ - fun frameId(frameId: String) = apply { body.frameId(frameId) } + fun frameId(frameId: String?) = apply { body.frameId(frameId) } + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) /** * Sets [Builder.frameId] to an arbitrary JSON value. @@ -343,7 +332,6 @@ private constructor( fun build(): SessionActParams = SessionActParams( id, - xSentAt, xStreamResponse, body.build(), additionalHeaders.build(), @@ -362,7 +350,6 @@ private constructor( override fun _headers(): Headers = Headers.builder() .apply { - xSentAt?.let { put("x-sent-at", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(it)) } xStreamResponse?.let { put("x-stream-response", it.toString()) } putAll(additionalHeaders) } @@ -489,7 +476,10 @@ private constructor( fun input(action: Action) = input(Input.ofAction(action)) /** Target frame ID for the action */ - fun frameId(frameId: String) = frameId(JsonField.of(frameId)) + fun frameId(frameId: String?) = frameId(JsonField.ofNullable(frameId)) + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) /** * Sets [Builder.frameId] to an arbitrary JSON value. @@ -752,7 +742,7 @@ private constructor( .toList() return when (bestMatches.size) { // This can happen if what we're deserializing is completely incompatible with - // all the possible variants (e.g. deserializing from array). + // all the possible variants (e.g. deserializing from boolean). 0 -> Input(_json = json) 1 -> bestMatches.single() // If there's more than one match with the highest validity, then use the first @@ -783,7 +773,7 @@ private constructor( class Options @JsonCreator(mode = JsonCreator.Mode.DISABLED) private constructor( - private val model: JsonField, + private val model: JsonField, private val timeout: JsonField, private val variables: JsonField, private val additionalProperties: MutableMap, @@ -791,7 +781,7 @@ private constructor( @JsonCreator private constructor( - @JsonProperty("model") @ExcludeMissing model: JsonField = JsonMissing.of(), + @JsonProperty("model") @ExcludeMissing model: JsonField = JsonMissing.of(), @JsonProperty("timeout") @ExcludeMissing timeout: JsonField = JsonMissing.of(), @JsonProperty("variables") @ExcludeMissing @@ -799,13 +789,12 @@ private constructor( ) : this(model, timeout, variables, mutableMapOf()) /** - * Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - * 'anthropic/claude-4.5-opus') + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') * * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if * the server responded with an unexpected value). */ - fun model(): Optional = model.getOptional("model") + fun model(): Optional = model.getOptional("model") /** * Timeout in ms for the action @@ -828,7 +817,7 @@ private constructor( * * Unlike [model], this method doesn't throw if the JSON field has an unexpected type. */ - @JsonProperty("model") @ExcludeMissing fun _model(): JsonField = model + @JsonProperty("model") @ExcludeMissing fun _model(): JsonField = model /** * Returns the raw JSON value of [timeout]. @@ -867,7 +856,7 @@ private constructor( /** A builder for [Options]. */ class Builder internal constructor() { - private var model: JsonField = JsonMissing.of() + private var model: JsonField = JsonMissing.of() private var timeout: JsonField = JsonMissing.of() private var variables: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @@ -880,29 +869,23 @@ private constructor( additionalProperties = options.additionalProperties.toMutableMap() } - /** - * Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - * 'anthropic/claude-4.5-opus') - */ - fun model(model: ModelConfig) = model(JsonField.of(model)) + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + fun model(model: Model) = model(JsonField.of(model)) /** * Sets [Builder.model] to an arbitrary JSON value. * - * You should usually call [Builder.model] with a well-typed [ModelConfig] value - * instead. This method is primarily for setting the field to an undocumented or not yet - * supported value. + * You should usually call [Builder.model] with a well-typed [Model] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. */ - fun model(model: JsonField) = apply { this.model = model } + fun model(model: JsonField) = apply { this.model = model } - /** Alias for calling [model] with `ModelConfig.ofName(name)`. */ - fun model(name: String) = model(ModelConfig.ofName(name)) + /** Alias for calling [model] with `Model.ofConfig(config)`. */ + fun model(config: ModelConfig) = model(Model.ofConfig(config)) - /** - * Alias for calling [model] with `ModelConfig.ofModelConfigObject(modelConfigObject)`. - */ - fun model(modelConfigObject: ModelConfig.ModelConfigObject) = - model(ModelConfig.ofModelConfigObject(modelConfigObject)) + /** Alias for calling [model] with `Model.ofString(string)`. */ + fun model(string: String) = model(Model.ofString(string)) /** Timeout in ms for the action */ fun timeout(timeout: Double) = timeout(JsonField.of(timeout)) @@ -989,6 +972,178 @@ private constructor( (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) + @JsonSerialize(using = Model.Serializer::class) + class Model + private constructor( + private val config: ModelConfig? = null, + private val string: String? = null, + private val _json: JsonValue? = null, + ) { + + fun config(): Optional = Optional.ofNullable(config) + + fun string(): Optional = Optional.ofNullable(string) + + fun isConfig(): Boolean = config != null + + fun isString(): Boolean = string != null + + fun asConfig(): ModelConfig = config.getOrThrow("config") + + fun asString(): String = string.getOrThrow("string") + + fun _json(): Optional = Optional.ofNullable(_json) + + fun accept(visitor: Visitor): T = + when { + config != null -> visitor.visitConfig(config) + string != null -> visitor.visitString(string) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + fun validate(): Model = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) { + config.validate() + } + + override fun visitString(string: String) {} + } + ) + 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 = + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) = config.validity() + + override fun visitString(string: String) = 1 + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Model && config == other.config && string == other.string + } + + override fun hashCode(): Int = Objects.hash(config, string) + + override fun toString(): String = + when { + config != null -> "Model{config=$config}" + string != null -> "Model{string=$string}" + _json != null -> "Model{_unknown=$_json}" + else -> throw IllegalStateException("Invalid Model") + } + + companion object { + + @JvmStatic fun ofConfig(config: ModelConfig) = Model(config = config) + + @JvmStatic fun ofString(string: String) = Model(string = string) + } + + /** + * An interface that defines how to map each variant of [Model] to a value of type [T]. + */ + interface Visitor { + + fun visitConfig(config: ModelConfig): T + + fun visitString(string: String): T + + /** + * Maps an unknown variant of [Model] to a value of type [T]. + * + * An instance of [Model] can contain an unknown variant if it was deserialized from + * data that doesn't match any known variant. For example, if the SDK is on an older + * version than the API, then the API may respond with new variants that the SDK is + * unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown Model: $json") + } + } + + internal class Deserializer : BaseDeserializer(Model::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): Model { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + Model(config = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef())?.let { + Model(string = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely incompatible + // with all the possible variants (e.g. deserializing from boolean). + 0 -> Model(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use the + // first completely valid match, or simply the first match if none are + // completely valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : BaseSerializer(Model::class) { + + override fun serialize( + value: Model, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.config != null -> generator.writeObject(value.config) + value.string != null -> generator.writeObject(value.string) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid Model") + } + } + } + } + /** Variables to substitute in the action instruction */ class Variables @JsonCreator @@ -1253,7 +1408,6 @@ private constructor( return other is SessionActParams && id == other.id && - xSentAt == other.xSentAt && xStreamResponse == other.xStreamResponse && body == other.body && additionalHeaders == other.additionalHeaders && @@ -1261,8 +1415,8 @@ private constructor( } override fun hashCode(): Int = - Objects.hash(id, xSentAt, xStreamResponse, body, additionalHeaders, additionalQueryParams) + Objects.hash(id, xStreamResponse, body, additionalHeaders, additionalQueryParams) override fun toString() = - "SessionActParams{id=$id, xSentAt=$xSentAt, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" + "SessionActParams{id=$id, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionEndParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionEndParams.kt index c861cc2..cf9a245 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionEndParams.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionEndParams.kt @@ -3,21 +3,14 @@ package com.browserbase.api.models.sessions import com.browserbase.api.core.Enum -import com.browserbase.api.core.ExcludeMissing import com.browserbase.api.core.JsonField -import com.browserbase.api.core.JsonMissing import com.browserbase.api.core.JsonValue import com.browserbase.api.core.Params 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 import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonProperty -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter -import java.util.Collections import java.util.Objects import java.util.Optional import kotlin.jvm.optionals.getOrNull @@ -26,25 +19,20 @@ import kotlin.jvm.optionals.getOrNull class SessionEndParams private constructor( private val id: String?, - private val xSentAt: OffsetDateTime?, private val xStreamResponse: XStreamResponse?, - private val body: Body, private val additionalHeaders: Headers, private val additionalQueryParams: QueryParams, + private val additionalBodyProperties: Map, ) : Params { /** Unique session identifier */ fun id(): Optional = Optional.ofNullable(id) - /** ISO timestamp when request was sent */ - fun xSentAt(): Optional = Optional.ofNullable(xSentAt) - /** Whether to stream the response via SSE */ fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) - fun __forceBody(): JsonValue = body.__forceBody() - - fun _additionalBodyProperties(): Map = body._additionalProperties() + /** Additional body properties to send with the request. */ + fun _additionalBodyProperties(): Map = additionalBodyProperties /** Additional headers to send with the request. */ fun _additionalHeaders(): Headers = additionalHeaders @@ -66,20 +54,18 @@ private constructor( class Builder internal constructor() { private var id: String? = null - private var xSentAt: OffsetDateTime? = null private var xStreamResponse: XStreamResponse? = null - private var body: Body.Builder = Body.builder() private var additionalHeaders: Headers.Builder = Headers.builder() private var additionalQueryParams: QueryParams.Builder = QueryParams.builder() + private var additionalBodyProperties: MutableMap = mutableMapOf() @JvmSynthetic internal fun from(sessionEndParams: SessionEndParams) = apply { id = sessionEndParams.id - xSentAt = sessionEndParams.xSentAt xStreamResponse = sessionEndParams.xStreamResponse - body = sessionEndParams.body.toBuilder() additionalHeaders = sessionEndParams.additionalHeaders.toBuilder() additionalQueryParams = sessionEndParams.additionalQueryParams.toBuilder() + additionalBodyProperties = sessionEndParams.additionalBodyProperties.toMutableMap() } /** Unique session identifier */ @@ -88,12 +74,6 @@ private constructor( /** Alias for calling [Builder.id] with `id.orElse(null)`. */ fun id(id: Optional) = id(id.getOrNull()) - /** ISO timestamp when request was sent */ - fun xSentAt(xSentAt: OffsetDateTime?) = apply { this.xSentAt = xSentAt } - - /** Alias for calling [Builder.xSentAt] with `xSentAt.orElse(null)`. */ - fun xSentAt(xSentAt: Optional) = xSentAt(xSentAt.getOrNull()) - /** Whether to stream the response via SSE */ fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { this.xStreamResponse = xStreamResponse @@ -103,36 +83,6 @@ private constructor( fun xStreamResponse(xStreamResponse: Optional) = xStreamResponse(xStreamResponse.getOrNull()) - /** - * Sets the entire request body. - * - * This is generally only useful if you are already constructing the body separately. - * Otherwise, it's more convenient to use the top-level setters instead: - * - [_forceBody] - */ - fun body(body: Body) = apply { this.body = body.toBuilder() } - - fun _forceBody(_forceBody: JsonValue) = apply { body._forceBody(_forceBody) } - - fun additionalBodyProperties(additionalBodyProperties: Map) = apply { - body.additionalProperties(additionalBodyProperties) - } - - fun putAdditionalBodyProperty(key: String, value: JsonValue) = apply { - body.putAdditionalProperty(key, value) - } - - fun putAllAdditionalBodyProperties(additionalBodyProperties: Map) = - apply { - body.putAllAdditionalProperties(additionalBodyProperties) - } - - fun removeAdditionalBodyProperty(key: String) = apply { body.removeAdditionalProperty(key) } - - fun removeAllAdditionalBodyProperties(keys: Set) = apply { - body.removeAllAdditionalProperties(keys) - } - fun additionalHeaders(additionalHeaders: Headers) = apply { this.additionalHeaders.clear() putAllAdditionalHeaders(additionalHeaders) @@ -231,6 +181,28 @@ private constructor( additionalQueryParams.removeAll(keys) } + fun additionalBodyProperties(additionalBodyProperties: Map) = apply { + this.additionalBodyProperties.clear() + putAllAdditionalBodyProperties(additionalBodyProperties) + } + + fun putAdditionalBodyProperty(key: String, value: JsonValue) = apply { + additionalBodyProperties.put(key, value) + } + + fun putAllAdditionalBodyProperties(additionalBodyProperties: Map) = + apply { + this.additionalBodyProperties.putAll(additionalBodyProperties) + } + + fun removeAdditionalBodyProperty(key: String) = apply { + additionalBodyProperties.remove(key) + } + + fun removeAllAdditionalBodyProperties(keys: Set) = apply { + keys.forEach(::removeAdditionalBodyProperty) + } + /** * Returns an immutable instance of [SessionEndParams]. * @@ -239,15 +211,15 @@ private constructor( fun build(): SessionEndParams = SessionEndParams( id, - xSentAt, xStreamResponse, - body.build(), additionalHeaders.build(), additionalQueryParams.build(), + additionalBodyProperties.toImmutable(), ) } - fun _body(): Body = body + fun _body(): Optional> = + Optional.ofNullable(additionalBodyProperties.ifEmpty { null }) fun _pathParam(index: Int): String = when (index) { @@ -258,7 +230,6 @@ private constructor( override fun _headers(): Headers = Headers.builder() .apply { - xSentAt?.let { put("x-sent-at", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(it)) } xStreamResponse?.let { put("x-stream-response", it.toString()) } putAll(additionalHeaders) } @@ -266,123 +237,6 @@ private constructor( override fun _queryParams(): QueryParams = additionalQueryParams - class Body - @JsonCreator(mode = JsonCreator.Mode.DISABLED) - private constructor( - private val _forceBody: JsonValue, - private val additionalProperties: MutableMap, - ) { - - @JsonCreator - private constructor( - @JsonProperty("_forceBody") @ExcludeMissing _forceBody: JsonValue = JsonMissing.of() - ) : this(_forceBody, mutableMapOf()) - - @JsonProperty("_forceBody") @ExcludeMissing fun __forceBody(): JsonValue = _forceBody - - @JsonAnySetter - private fun putAdditionalProperty(key: String, value: JsonValue) { - additionalProperties.put(key, value) - } - - @JsonAnyGetter - @ExcludeMissing - fun _additionalProperties(): Map = - Collections.unmodifiableMap(additionalProperties) - - fun toBuilder() = Builder().from(this) - - companion object { - - /** Returns a mutable builder for constructing an instance of [Body]. */ - @JvmStatic fun builder() = Builder() - } - - /** A builder for [Body]. */ - class Builder internal constructor() { - - private var _forceBody: JsonValue = JsonMissing.of() - private var additionalProperties: MutableMap = mutableMapOf() - - @JvmSynthetic - internal fun from(body: Body) = apply { - _forceBody = body._forceBody - additionalProperties = body.additionalProperties.toMutableMap() - } - - fun _forceBody(_forceBody: JsonValue) = apply { this._forceBody = _forceBody } - - 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 [Body]. - * - * Further updates to this [Builder] will not mutate the returned instance. - */ - fun build(): Body = Body(_forceBody, additionalProperties.toMutableMap()) - } - - private var validated: Boolean = false - - fun validate(): Body = 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 = 0 - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - - return other is Body && - _forceBody == other._forceBody && - additionalProperties == other.additionalProperties - } - - private val hashCode: Int by lazy { Objects.hash(_forceBody, additionalProperties) } - - override fun hashCode(): Int = hashCode - - override fun toString() = - "Body{_forceBody=$_forceBody, additionalProperties=$additionalProperties}" - } - /** Whether to stream the response via SSE */ class XStreamResponse @JsonCreator private constructor(private val value: JsonField) : Enum { @@ -522,16 +376,21 @@ private constructor( return other is SessionEndParams && id == other.id && - xSentAt == other.xSentAt && xStreamResponse == other.xStreamResponse && - body == other.body && additionalHeaders == other.additionalHeaders && - additionalQueryParams == other.additionalQueryParams + additionalQueryParams == other.additionalQueryParams && + additionalBodyProperties == other.additionalBodyProperties } override fun hashCode(): Int = - Objects.hash(id, xSentAt, xStreamResponse, body, additionalHeaders, additionalQueryParams) + Objects.hash( + id, + xStreamResponse, + additionalHeaders, + additionalQueryParams, + additionalBodyProperties, + ) override fun toString() = - "SessionEndParams{id=$id, xSentAt=$xSentAt, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" + "SessionEndParams{id=$id, xStreamResponse=$xStreamResponse, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams, additionalBodyProperties=$additionalBodyProperties}" } 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 813b432..b8a795f 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 @@ -2,13 +2,17 @@ package com.browserbase.api.models.sessions +import com.browserbase.api.core.BaseDeserializer +import com.browserbase.api.core.BaseSerializer import com.browserbase.api.core.Enum import com.browserbase.api.core.ExcludeMissing import com.browserbase.api.core.JsonField import com.browserbase.api.core.JsonMissing import com.browserbase.api.core.JsonValue import com.browserbase.api.core.Params +import com.browserbase.api.core.allMaxBy import com.browserbase.api.core.checkRequired +import com.browserbase.api.core.getOrThrow import com.browserbase.api.core.http.Headers import com.browserbase.api.core.http.QueryParams import com.browserbase.api.errors.StagehandInvalidDataException @@ -16,8 +20,13 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter import com.fasterxml.jackson.annotation.JsonAnySetter import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.ObjectCodec +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef import java.util.Collections import java.util.Objects import java.util.Optional @@ -27,7 +36,6 @@ import kotlin.jvm.optionals.getOrNull class SessionExecuteParams private constructor( private val id: String?, - private val xSentAt: OffsetDateTime?, private val xStreamResponse: XStreamResponse?, private val body: Body, private val additionalHeaders: Headers, @@ -37,9 +45,6 @@ private constructor( /** Unique session identifier */ fun id(): Optional = Optional.ofNullable(id) - /** ISO timestamp when request was sent */ - fun xSentAt(): Optional = Optional.ofNullable(xSentAt) - /** Whether to stream the response via SSE */ fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) @@ -63,6 +68,14 @@ private constructor( */ fun frameId(): Optional = body.frameId() + /** + * If true, the server captures a cache entry and returns it to the client + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). + */ + fun shouldCache(): Optional = body.shouldCache() + /** * Returns the raw JSON value of [agentConfig]. * @@ -84,6 +97,13 @@ private constructor( */ fun _frameId(): JsonField = body._frameId() + /** + * Returns the raw JSON value of [shouldCache]. + * + * Unlike [shouldCache], this method doesn't throw if the JSON field has an unexpected type. + */ + fun _shouldCache(): JsonField = body._shouldCache() + fun _additionalBodyProperties(): Map = body._additionalProperties() /** Additional headers to send with the request. */ @@ -112,7 +132,6 @@ private constructor( class Builder internal constructor() { private var id: String? = null - private var xSentAt: OffsetDateTime? = null private var xStreamResponse: XStreamResponse? = null private var body: Body.Builder = Body.builder() private var additionalHeaders: Headers.Builder = Headers.builder() @@ -121,7 +140,6 @@ private constructor( @JvmSynthetic internal fun from(sessionExecuteParams: SessionExecuteParams) = apply { id = sessionExecuteParams.id - xSentAt = sessionExecuteParams.xSentAt xStreamResponse = sessionExecuteParams.xStreamResponse body = sessionExecuteParams.body.toBuilder() additionalHeaders = sessionExecuteParams.additionalHeaders.toBuilder() @@ -134,12 +152,6 @@ private constructor( /** Alias for calling [Builder.id] with `id.orElse(null)`. */ fun id(id: Optional) = id(id.getOrNull()) - /** ISO timestamp when request was sent */ - fun xSentAt(xSentAt: OffsetDateTime?) = apply { this.xSentAt = xSentAt } - - /** Alias for calling [Builder.xSentAt] with `xSentAt.orElse(null)`. */ - fun xSentAt(xSentAt: Optional) = xSentAt(xSentAt.getOrNull()) - /** Whether to stream the response via SSE */ fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { this.xStreamResponse = xStreamResponse @@ -157,6 +169,7 @@ private constructor( * - [agentConfig] * - [executeOptions] * - [frameId] + * - [shouldCache] */ fun body(body: Body) = apply { this.body = body.toBuilder() } @@ -189,7 +202,10 @@ private constructor( } /** Target frame ID for the agent */ - fun frameId(frameId: String) = apply { body.frameId(frameId) } + fun frameId(frameId: String?) = apply { body.frameId(frameId) } + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) /** * Sets [Builder.frameId] to an arbitrary JSON value. @@ -199,6 +215,18 @@ private constructor( */ fun frameId(frameId: JsonField) = apply { body.frameId(frameId) } + /** If true, the server captures a cache entry and returns it to the client */ + fun shouldCache(shouldCache: Boolean) = apply { body.shouldCache(shouldCache) } + + /** + * Sets [Builder.shouldCache] to an arbitrary JSON value. + * + * You should usually call [Builder.shouldCache] with a well-typed [Boolean] value instead. + * This method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun shouldCache(shouldCache: JsonField) = apply { body.shouldCache(shouldCache) } + fun additionalBodyProperties(additionalBodyProperties: Map) = apply { body.additionalProperties(additionalBodyProperties) } @@ -332,7 +360,6 @@ private constructor( fun build(): SessionExecuteParams = SessionExecuteParams( id, - xSentAt, xStreamResponse, body.build(), additionalHeaders.build(), @@ -351,7 +378,6 @@ private constructor( override fun _headers(): Headers = Headers.builder() .apply { - xSentAt?.let { put("x-sent-at", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(it)) } xStreamResponse?.let { put("x-stream-response", it.toString()) } putAll(additionalHeaders) } @@ -365,6 +391,7 @@ private constructor( private val agentConfig: JsonField, private val executeOptions: JsonField, private val frameId: JsonField, + private val shouldCache: JsonField, private val additionalProperties: MutableMap, ) { @@ -377,7 +404,10 @@ private constructor( @ExcludeMissing executeOptions: JsonField = JsonMissing.of(), @JsonProperty("frameId") @ExcludeMissing frameId: JsonField = JsonMissing.of(), - ) : this(agentConfig, executeOptions, frameId, mutableMapOf()) + @JsonProperty("shouldCache") + @ExcludeMissing + shouldCache: JsonField = JsonMissing.of(), + ) : this(agentConfig, executeOptions, frameId, shouldCache, mutableMapOf()) /** * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is @@ -399,6 +429,14 @@ private constructor( */ fun frameId(): Optional = frameId.getOptional("frameId") + /** + * If true, the server captures a cache entry and returns it to the client + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun shouldCache(): Optional = shouldCache.getOptional("shouldCache") + /** * Returns the raw JSON value of [agentConfig]. * @@ -425,6 +463,15 @@ private constructor( */ @JsonProperty("frameId") @ExcludeMissing fun _frameId(): JsonField = frameId + /** + * Returns the raw JSON value of [shouldCache]. + * + * Unlike [shouldCache], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("shouldCache") + @ExcludeMissing + fun _shouldCache(): JsonField = shouldCache + @JsonAnySetter private fun putAdditionalProperty(key: String, value: JsonValue) { additionalProperties.put(key, value) @@ -457,6 +504,7 @@ private constructor( private var agentConfig: JsonField? = null private var executeOptions: JsonField? = null private var frameId: JsonField = JsonMissing.of() + private var shouldCache: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @JvmSynthetic @@ -464,6 +512,7 @@ private constructor( agentConfig = body.agentConfig executeOptions = body.executeOptions frameId = body.frameId + shouldCache = body.shouldCache additionalProperties = body.additionalProperties.toMutableMap() } @@ -495,7 +544,10 @@ private constructor( } /** Target frame ID for the agent */ - fun frameId(frameId: String) = frameId(JsonField.of(frameId)) + fun frameId(frameId: String?) = frameId(JsonField.ofNullable(frameId)) + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) /** * Sets [Builder.frameId] to an arbitrary JSON value. @@ -506,6 +558,20 @@ private constructor( */ fun frameId(frameId: JsonField) = apply { this.frameId = frameId } + /** If true, the server captures a cache entry and returns it to the client */ + fun shouldCache(shouldCache: Boolean) = shouldCache(JsonField.of(shouldCache)) + + /** + * Sets [Builder.shouldCache] to an arbitrary JSON value. + * + * You should usually call [Builder.shouldCache] with a well-typed [Boolean] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun shouldCache(shouldCache: JsonField) = apply { + this.shouldCache = shouldCache + } + fun additionalProperties(additionalProperties: Map) = apply { this.additionalProperties.clear() putAllAdditionalProperties(additionalProperties) @@ -543,6 +609,7 @@ private constructor( checkRequired("agentConfig", agentConfig), checkRequired("executeOptions", executeOptions), frameId, + shouldCache, additionalProperties.toMutableMap(), ) } @@ -557,6 +624,7 @@ private constructor( agentConfig().validate() executeOptions().validate() frameId() + shouldCache() validated = true } @@ -578,7 +646,8 @@ private constructor( internal fun validity(): Int = (agentConfig.asKnown().getOrNull()?.validity() ?: 0) + (executeOptions.asKnown().getOrNull()?.validity() ?: 0) + - (if (frameId.asKnown().isPresent) 1 else 0) + (if (frameId.asKnown().isPresent) 1 else 0) + + (if (shouldCache.asKnown().isPresent) 1 else 0) override fun equals(other: Any?): Boolean { if (this === other) { @@ -589,24 +658,27 @@ private constructor( agentConfig == other.agentConfig && executeOptions == other.executeOptions && frameId == other.frameId && + shouldCache == other.shouldCache && additionalProperties == other.additionalProperties } private val hashCode: Int by lazy { - Objects.hash(agentConfig, executeOptions, frameId, additionalProperties) + Objects.hash(agentConfig, executeOptions, frameId, shouldCache, additionalProperties) } override fun hashCode(): Int = hashCode override fun toString() = - "Body{agentConfig=$agentConfig, executeOptions=$executeOptions, frameId=$frameId, additionalProperties=$additionalProperties}" + "Body{agentConfig=$agentConfig, executeOptions=$executeOptions, frameId=$frameId, shouldCache=$shouldCache, additionalProperties=$additionalProperties}" } class AgentConfig @JsonCreator(mode = JsonCreator.Mode.DISABLED) private constructor( private val cua: JsonField, - private val model: JsonField, + private val executionModel: JsonField, + private val mode: JsonField, + private val model: JsonField, private val provider: JsonField, private val systemPrompt: JsonField, private val additionalProperties: MutableMap, @@ -615,17 +687,21 @@ private constructor( @JsonCreator private constructor( @JsonProperty("cua") @ExcludeMissing cua: JsonField = JsonMissing.of(), - @JsonProperty("model") @ExcludeMissing model: JsonField = JsonMissing.of(), + @JsonProperty("executionModel") + @ExcludeMissing + executionModel: JsonField = JsonMissing.of(), + @JsonProperty("mode") @ExcludeMissing mode: JsonField = JsonMissing.of(), + @JsonProperty("model") @ExcludeMissing model: JsonField = JsonMissing.of(), @JsonProperty("provider") @ExcludeMissing provider: JsonField = JsonMissing.of(), @JsonProperty("systemPrompt") @ExcludeMissing systemPrompt: JsonField = JsonMissing.of(), - ) : this(cua, model, provider, systemPrompt, mutableMapOf()) + ) : this(cua, executionModel, mode, model, provider, systemPrompt, mutableMapOf()) /** - * Enable Computer Use Agent mode + * Deprecated. Use mode: 'cua' instead. If both are provided, mode takes precedence. * * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if * the server responded with an unexpected value). @@ -633,13 +709,31 @@ private constructor( fun cua(): Optional = cua.getOptional("cua") /** - * Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - * 'anthropic/claude-4.5-opus') + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') for tool + * execution (observe/act calls within agent tools). If not specified, inherits from the + * main model configuration. * * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if * the server responded with an unexpected value). */ - fun model(): Optional = model.getOptional("model") + fun executionModel(): Optional = + executionModel.getOptional("executionModel") + + /** + * Tool mode for the agent (dom, hybrid, cua). If set, overrides cua. + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun mode(): Optional = mode.getOptional("mode") + + /** + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun model(): Optional = model.getOptional("model") /** * AI provider for the agent (legacy, use model: openai/gpt-5-nano instead) @@ -664,12 +758,29 @@ private constructor( */ @JsonProperty("cua") @ExcludeMissing fun _cua(): JsonField = cua + /** + * Returns the raw JSON value of [executionModel]. + * + * Unlike [executionModel], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("executionModel") + @ExcludeMissing + fun _executionModel(): JsonField = executionModel + + /** + * Returns the raw JSON value of [mode]. + * + * Unlike [mode], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("mode") @ExcludeMissing fun _mode(): JsonField = mode + /** * Returns the raw JSON value of [model]. * * Unlike [model], this method doesn't throw if the JSON field has an unexpected type. */ - @JsonProperty("model") @ExcludeMissing fun _model(): JsonField = model + @JsonProperty("model") @ExcludeMissing fun _model(): JsonField = model /** * Returns the raw JSON value of [provider]. @@ -710,7 +821,9 @@ private constructor( class Builder internal constructor() { private var cua: JsonField = JsonMissing.of() - private var model: JsonField = JsonMissing.of() + private var executionModel: JsonField = JsonMissing.of() + private var mode: JsonField = JsonMissing.of() + private var model: JsonField = JsonMissing.of() private var provider: JsonField = JsonMissing.of() private var systemPrompt: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @@ -718,13 +831,15 @@ private constructor( @JvmSynthetic internal fun from(agentConfig: AgentConfig) = apply { cua = agentConfig.cua + executionModel = agentConfig.executionModel + mode = agentConfig.mode model = agentConfig.model provider = agentConfig.provider systemPrompt = agentConfig.systemPrompt additionalProperties = agentConfig.additionalProperties.toMutableMap() } - /** Enable Computer Use Agent mode */ + /** Deprecated. Use mode: 'cua' instead. If both are provided, mode takes precedence. */ fun cua(cua: Boolean) = cua(JsonField.of(cua)) /** @@ -737,28 +852,62 @@ private constructor( fun cua(cua: JsonField) = apply { this.cua = cua } /** - * Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - * 'anthropic/claude-4.5-opus') + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') for tool + * execution (observe/act calls within agent tools). If not specified, inherits from the + * main model configuration. */ - fun model(model: ModelConfig) = model(JsonField.of(model)) + fun executionModel(executionModel: ExecutionModel) = + executionModel(JsonField.of(executionModel)) /** - * Sets [Builder.model] to an arbitrary JSON value. + * Sets [Builder.executionModel] to an arbitrary JSON value. * - * You should usually call [Builder.model] with a well-typed [ModelConfig] value - * instead. This method is primarily for setting the field to an undocumented or not yet - * supported value. + * You should usually call [Builder.executionModel] with a well-typed [ExecutionModel] + * value instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun executionModel(executionModel: JsonField) = apply { + this.executionModel = executionModel + } + + /** + * Alias for calling [executionModel] with `ExecutionModel.ofModelConfig(modelConfig)`. */ - fun model(model: JsonField) = apply { this.model = model } + fun executionModel(modelConfig: ModelConfig) = + executionModel(ExecutionModel.ofModelConfig(modelConfig)) - /** Alias for calling [model] with `ModelConfig.ofName(name)`. */ - fun model(name: String) = model(ModelConfig.ofName(name)) + /** Alias for calling [executionModel] with `ExecutionModel.ofString(string)`. */ + fun executionModel(string: String) = executionModel(ExecutionModel.ofString(string)) + + /** Tool mode for the agent (dom, hybrid, cua). If set, overrides cua. */ + fun mode(mode: Mode) = mode(JsonField.of(mode)) /** - * Alias for calling [model] with `ModelConfig.ofModelConfigObject(modelConfigObject)`. + * Sets [Builder.mode] to an arbitrary JSON value. + * + * You should usually call [Builder.mode] with a well-typed [Mode] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. */ - fun model(modelConfigObject: ModelConfig.ModelConfigObject) = - model(ModelConfig.ofModelConfigObject(modelConfigObject)) + fun mode(mode: JsonField) = apply { this.mode = mode } + + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + fun model(model: Model) = model(JsonField.of(model)) + + /** + * Sets [Builder.model] to an arbitrary JSON value. + * + * You should usually call [Builder.model] with a well-typed [Model] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. + */ + fun model(model: JsonField) = apply { this.model = model } + + /** Alias for calling [model] with `Model.ofConfig(config)`. */ + fun model(config: ModelConfig) = model(Model.ofConfig(config)) + + /** Alias for calling [model] with `Model.ofString(string)`. */ + fun model(string: String) = model(Model.ofString(string)) /** AI provider for the agent (legacy, use model: openai/gpt-5-nano instead) */ fun provider(provider: Provider) = provider(JsonField.of(provider)) @@ -811,7 +960,15 @@ private constructor( * Further updates to this [Builder] will not mutate the returned instance. */ fun build(): AgentConfig = - AgentConfig(cua, model, provider, systemPrompt, additionalProperties.toMutableMap()) + AgentConfig( + cua, + executionModel, + mode, + model, + provider, + systemPrompt, + additionalProperties.toMutableMap(), + ) } private var validated: Boolean = false @@ -822,6 +979,8 @@ private constructor( } cua() + executionModel().ifPresent { it.validate() } + mode().ifPresent { it.validate() } model().ifPresent { it.validate() } provider().ifPresent { it.validate() } systemPrompt() @@ -845,10 +1004,500 @@ private constructor( @JvmSynthetic internal fun validity(): Int = (if (cua.asKnown().isPresent) 1 else 0) + + (executionModel.asKnown().getOrNull()?.validity() ?: 0) + + (mode.asKnown().getOrNull()?.validity() ?: 0) + (model.asKnown().getOrNull()?.validity() ?: 0) + (provider.asKnown().getOrNull()?.validity() ?: 0) + (if (systemPrompt.asKnown().isPresent) 1 else 0) + /** + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') for tool + * execution (observe/act calls within agent tools). If not specified, inherits from the + * main model configuration. + */ + @JsonDeserialize(using = ExecutionModel.Deserializer::class) + @JsonSerialize(using = ExecutionModel.Serializer::class) + class ExecutionModel + private constructor( + private val modelConfig: ModelConfig? = null, + private val string: String? = null, + private val _json: JsonValue? = null, + ) { + + fun modelConfig(): Optional = Optional.ofNullable(modelConfig) + + fun string(): Optional = Optional.ofNullable(string) + + fun isModelConfig(): Boolean = modelConfig != null + + fun isString(): Boolean = string != null + + fun asModelConfig(): ModelConfig = modelConfig.getOrThrow("modelConfig") + + fun asString(): String = string.getOrThrow("string") + + fun _json(): Optional = Optional.ofNullable(_json) + + fun accept(visitor: Visitor): T = + when { + modelConfig != null -> visitor.visitModelConfig(modelConfig) + string != null -> visitor.visitString(string) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + fun validate(): ExecutionModel = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitModelConfig(modelConfig: ModelConfig) { + modelConfig.validate() + } + + override fun visitString(string: String) {} + } + ) + 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 = + accept( + object : Visitor { + override fun visitModelConfig(modelConfig: ModelConfig) = + modelConfig.validity() + + override fun visitString(string: String) = 1 + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is ExecutionModel && + modelConfig == other.modelConfig && + string == other.string + } + + override fun hashCode(): Int = Objects.hash(modelConfig, string) + + override fun toString(): String = + when { + modelConfig != null -> "ExecutionModel{modelConfig=$modelConfig}" + string != null -> "ExecutionModel{string=$string}" + _json != null -> "ExecutionModel{_unknown=$_json}" + else -> throw IllegalStateException("Invalid ExecutionModel") + } + + companion object { + + @JvmStatic + fun ofModelConfig(modelConfig: ModelConfig) = + ExecutionModel(modelConfig = modelConfig) + + @JvmStatic fun ofString(string: String) = ExecutionModel(string = string) + } + + /** + * An interface that defines how to map each variant of [ExecutionModel] to a value of + * type [T]. + */ + interface Visitor { + + fun visitModelConfig(modelConfig: ModelConfig): T + + fun visitString(string: String): T + + /** + * Maps an unknown variant of [ExecutionModel] to a value of type [T]. + * + * An instance of [ExecutionModel] can contain an unknown variant if it was + * deserialized from data that doesn't match any known variant. For example, if the + * SDK is on an older version than the API, then the API may respond with new + * variants that the SDK is unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown ExecutionModel: $json") + } + } + + internal class Deserializer : BaseDeserializer(ExecutionModel::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): ExecutionModel { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + ExecutionModel(modelConfig = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef())?.let { + ExecutionModel(string = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely incompatible + // with all the possible variants (e.g. deserializing from boolean). + 0 -> ExecutionModel(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use the + // first completely valid match, or simply the first match if none are + // completely valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : BaseSerializer(ExecutionModel::class) { + + override fun serialize( + value: ExecutionModel, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.modelConfig != null -> generator.writeObject(value.modelConfig) + value.string != null -> generator.writeObject(value.string) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid ExecutionModel") + } + } + } + } + + /** Tool mode for the agent (dom, hybrid, cua). If set, overrides cua. */ + class Mode @JsonCreator private constructor(private val value: JsonField) : Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is + * on an older version than the API, then the API may respond with new members that the + * SDK is unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val DOM = of("dom") + + @JvmField val HYBRID = of("hybrid") + + @JvmField val CUA = of("cua") + + @JvmStatic fun of(value: String) = Mode(JsonField.of(value)) + } + + /** An enum containing [Mode]'s known values. */ + enum class Known { + DOM, + HYBRID, + CUA, + } + + /** + * An enum containing [Mode]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [Mode] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if + * the SDK is on an older version than the API, then the API may respond with new + * members that the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + DOM, + HYBRID, + CUA, + /** An enum member indicating that [Mode] was instantiated with an unknown value. */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or + * [Value._UNKNOWN] if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you + * want to throw for the unknown case. + */ + fun value(): Value = + when (this) { + DOM -> Value.DOM + HYBRID -> Value.HYBRID + CUA -> Value.CUA + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and + * don't want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + DOM -> Known.DOM + HYBRID -> Known.HYBRID + CUA -> Known.CUA + else -> throw StagehandInvalidDataException("Unknown Mode: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for + * debugging and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have + * the expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + fun validate(): Mode = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Mode && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + @JsonDeserialize(using = Model.Deserializer::class) + @JsonSerialize(using = Model.Serializer::class) + class Model + private constructor( + private val config: ModelConfig? = null, + private val string: String? = null, + private val _json: JsonValue? = null, + ) { + + fun config(): Optional = Optional.ofNullable(config) + + fun string(): Optional = Optional.ofNullable(string) + + fun isConfig(): Boolean = config != null + + fun isString(): Boolean = string != null + + fun asConfig(): ModelConfig = config.getOrThrow("config") + + fun asString(): String = string.getOrThrow("string") + + fun _json(): Optional = Optional.ofNullable(_json) + + fun accept(visitor: Visitor): T = + when { + config != null -> visitor.visitConfig(config) + string != null -> visitor.visitString(string) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + fun validate(): Model = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) { + config.validate() + } + + override fun visitString(string: String) {} + } + ) + 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 = + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) = config.validity() + + override fun visitString(string: String) = 1 + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Model && config == other.config && string == other.string + } + + override fun hashCode(): Int = Objects.hash(config, string) + + override fun toString(): String = + when { + config != null -> "Model{config=$config}" + string != null -> "Model{string=$string}" + _json != null -> "Model{_unknown=$_json}" + else -> throw IllegalStateException("Invalid Model") + } + + companion object { + + @JvmStatic fun ofConfig(config: ModelConfig) = Model(config = config) + + @JvmStatic fun ofString(string: String) = Model(string = string) + } + + /** + * An interface that defines how to map each variant of [Model] to a value of type [T]. + */ + interface Visitor { + + fun visitConfig(config: ModelConfig): T + + fun visitString(string: String): T + + /** + * Maps an unknown variant of [Model] to a value of type [T]. + * + * An instance of [Model] can contain an unknown variant if it was deserialized from + * data that doesn't match any known variant. For example, if the SDK is on an older + * version than the API, then the API may respond with new variants that the SDK is + * unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown Model: $json") + } + } + + internal class Deserializer : BaseDeserializer(Model::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): Model { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + Model(config = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef())?.let { + Model(string = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely incompatible + // with all the possible variants (e.g. deserializing from boolean). + 0 -> Model(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use the + // first completely valid match, or simply the first match if none are + // completely valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : BaseSerializer(Model::class) { + + override fun serialize( + value: Model, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.config != null -> generator.writeObject(value.config) + value.string != null -> generator.writeObject(value.string) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid Model") + } + } + } + } + /** AI provider for the agent (legacy, use model: openai/gpt-5-nano instead) */ class Provider @JsonCreator private constructor(private val value: JsonField) : Enum { @@ -999,6 +1648,8 @@ private constructor( return other is AgentConfig && cua == other.cua && + executionModel == other.executionModel && + mode == other.mode && model == other.model && provider == other.provider && systemPrompt == other.systemPrompt && @@ -1006,13 +1657,21 @@ private constructor( } private val hashCode: Int by lazy { - Objects.hash(cua, model, provider, systemPrompt, additionalProperties) + Objects.hash( + cua, + executionModel, + mode, + model, + provider, + systemPrompt, + additionalProperties, + ) } override fun hashCode(): Int = hashCode override fun toString() = - "AgentConfig{cua=$cua, model=$model, provider=$provider, systemPrompt=$systemPrompt, additionalProperties=$additionalProperties}" + "AgentConfig{cua=$cua, executionModel=$executionModel, mode=$mode, model=$model, provider=$provider, systemPrompt=$systemPrompt, additionalProperties=$additionalProperties}" } class ExecuteOptions @@ -1401,7 +2060,6 @@ private constructor( return other is SessionExecuteParams && id == other.id && - xSentAt == other.xSentAt && xStreamResponse == other.xStreamResponse && body == other.body && additionalHeaders == other.additionalHeaders && @@ -1409,8 +2067,8 @@ private constructor( } override fun hashCode(): Int = - Objects.hash(id, xSentAt, xStreamResponse, body, additionalHeaders, additionalQueryParams) + Objects.hash(id, xStreamResponse, body, additionalHeaders, additionalQueryParams) override fun toString() = - "SessionExecuteParams{id=$id, xSentAt=$xSentAt, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" + "SessionExecuteParams{id=$id, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponse.kt index 8718994..9b16655 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponse.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponse.kt @@ -195,13 +195,17 @@ private constructor( @JsonCreator(mode = JsonCreator.Mode.DISABLED) private constructor( private val result: JsonField, + private val cacheEntry: JsonField, private val additionalProperties: MutableMap, ) { @JsonCreator private constructor( - @JsonProperty("result") @ExcludeMissing result: JsonField = JsonMissing.of() - ) : this(result, mutableMapOf()) + @JsonProperty("result") @ExcludeMissing result: JsonField = JsonMissing.of(), + @JsonProperty("cacheEntry") + @ExcludeMissing + cacheEntry: JsonField = JsonMissing.of(), + ) : this(result, cacheEntry, mutableMapOf()) /** * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is @@ -209,6 +213,12 @@ private constructor( */ fun result(): Result = result.getRequired("result") + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun cacheEntry(): Optional = cacheEntry.getOptional("cacheEntry") + /** * Returns the raw JSON value of [result]. * @@ -216,6 +226,15 @@ private constructor( */ @JsonProperty("result") @ExcludeMissing fun _result(): JsonField = result + /** + * Returns the raw JSON value of [cacheEntry]. + * + * Unlike [cacheEntry], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("cacheEntry") + @ExcludeMissing + fun _cacheEntry(): JsonField = cacheEntry + @JsonAnySetter private fun putAdditionalProperty(key: String, value: JsonValue) { additionalProperties.put(key, value) @@ -245,11 +264,13 @@ private constructor( class Builder internal constructor() { private var result: JsonField? = null + private var cacheEntry: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @JvmSynthetic internal fun from(data: Data) = apply { result = data.result + cacheEntry = data.cacheEntry additionalProperties = data.additionalProperties.toMutableMap() } @@ -264,6 +285,19 @@ private constructor( */ fun result(result: JsonField) = apply { this.result = result } + fun cacheEntry(cacheEntry: CacheEntry) = cacheEntry(JsonField.of(cacheEntry)) + + /** + * Sets [Builder.cacheEntry] to an arbitrary JSON value. + * + * You should usually call [Builder.cacheEntry] with a well-typed [CacheEntry] value + * instead. This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun cacheEntry(cacheEntry: JsonField) = apply { + this.cacheEntry = cacheEntry + } + fun additionalProperties(additionalProperties: Map) = apply { this.additionalProperties.clear() putAllAdditionalProperties(additionalProperties) @@ -296,7 +330,11 @@ private constructor( * @throws IllegalStateException if any required field is unset. */ fun build(): Data = - Data(checkRequired("result", result), additionalProperties.toMutableMap()) + Data( + checkRequired("result", result), + cacheEntry, + additionalProperties.toMutableMap(), + ) } private var validated: Boolean = false @@ -307,6 +345,7 @@ private constructor( } result().validate() + cacheEntry().ifPresent { it.validate() } validated = true } @@ -324,7 +363,10 @@ private constructor( * * Used for best match union deserialization. */ - @JvmSynthetic internal fun validity(): Int = (result.asKnown().getOrNull()?.validity() ?: 0) + @JvmSynthetic + internal fun validity(): Int = + (result.asKnown().getOrNull()?.validity() ?: 0) + + (cacheEntry.asKnown().getOrNull()?.validity() ?: 0) class Result @JsonCreator(mode = JsonCreator.Mode.DISABLED) @@ -1621,6 +1663,197 @@ private constructor( "Result{actions=$actions, completed=$completed, message=$message, success=$success, metadata=$metadata, usage=$usage, additionalProperties=$additionalProperties}" } + class CacheEntry + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val cacheKey: JsonField, + private val entry: JsonValue, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("cacheKey") + @ExcludeMissing + cacheKey: JsonField = JsonMissing.of(), + @JsonProperty("entry") @ExcludeMissing entry: JsonValue = JsonMissing.of(), + ) : this(cacheKey, entry, mutableMapOf()) + + /** + * Opaque cache identifier computed from instruction, URL, options, and config + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected + * value). + */ + fun cacheKey(): String = cacheKey.getRequired("cacheKey") + + /** + * Serialized cache entry that can be written to disk + * + * This arbitrary value can be deserialized into a custom type using the `convert` + * method: + * ```java + * MyClass myObject = cacheEntry.entry().convert(MyClass.class); + * ``` + */ + @JsonProperty("entry") @ExcludeMissing fun _entry(): JsonValue = entry + + /** + * Returns the raw JSON value of [cacheKey]. + * + * Unlike [cacheKey], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("cacheKey") @ExcludeMissing fun _cacheKey(): JsonField = cacheKey + + @JsonAnySetter + private fun putAdditionalProperty(key: String, value: JsonValue) { + additionalProperties.put(key, value) + } + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = + Collections.unmodifiableMap(additionalProperties) + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [CacheEntry]. + * + * The following fields are required: + * ```java + * .cacheKey() + * .entry() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [CacheEntry]. */ + class Builder internal constructor() { + + private var cacheKey: JsonField? = null + private var entry: JsonValue? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(cacheEntry: CacheEntry) = apply { + cacheKey = cacheEntry.cacheKey + entry = cacheEntry.entry + additionalProperties = cacheEntry.additionalProperties.toMutableMap() + } + + /** Opaque cache identifier computed from instruction, URL, options, and config */ + fun cacheKey(cacheKey: String) = cacheKey(JsonField.of(cacheKey)) + + /** + * Sets [Builder.cacheKey] to an arbitrary JSON value. + * + * You should usually call [Builder.cacheKey] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun cacheKey(cacheKey: JsonField) = apply { this.cacheKey = cacheKey } + + /** Serialized cache entry that can be written to disk */ + fun entry(entry: JsonValue) = apply { this.entry = entry } + + 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 [CacheEntry]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .cacheKey() + * .entry() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): CacheEntry = + CacheEntry( + checkRequired("cacheKey", cacheKey), + checkRequired("entry", entry), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + fun validate(): CacheEntry = apply { + if (validated) { + return@apply + } + + cacheKey() + 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 = (if (cacheKey.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is CacheEntry && + cacheKey == other.cacheKey && + entry == other.entry && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(cacheKey, entry, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "CacheEntry{cacheKey=$cacheKey, entry=$entry, additionalProperties=$additionalProperties}" + } + override fun equals(other: Any?): Boolean { if (this === other) { return true @@ -1628,14 +1861,16 @@ private constructor( return other is Data && result == other.result && + cacheEntry == other.cacheEntry && additionalProperties == other.additionalProperties } - private val hashCode: Int by lazy { Objects.hash(result, additionalProperties) } + private val hashCode: Int by lazy { Objects.hash(result, cacheEntry, additionalProperties) } override fun hashCode(): Int = hashCode - override fun toString() = "Data{result=$result, additionalProperties=$additionalProperties}" + override fun toString() = + "Data{result=$result, cacheEntry=$cacheEntry, additionalProperties=$additionalProperties}" } override fun equals(other: Any?): Boolean { diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractParams.kt index 3ef55df..d732dbe 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractParams.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractParams.kt @@ -2,12 +2,16 @@ package com.browserbase.api.models.sessions +import com.browserbase.api.core.BaseDeserializer +import com.browserbase.api.core.BaseSerializer import com.browserbase.api.core.Enum import com.browserbase.api.core.ExcludeMissing import com.browserbase.api.core.JsonField import com.browserbase.api.core.JsonMissing import com.browserbase.api.core.JsonValue import com.browserbase.api.core.Params +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 @@ -16,8 +20,13 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter import com.fasterxml.jackson.annotation.JsonAnySetter import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.ObjectCodec +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef import java.util.Collections import java.util.Objects import java.util.Optional @@ -27,7 +36,6 @@ import kotlin.jvm.optionals.getOrNull class SessionExtractParams private constructor( private val id: String?, - private val xSentAt: OffsetDateTime?, private val xStreamResponse: XStreamResponse?, private val body: Body, private val additionalHeaders: Headers, @@ -37,9 +45,6 @@ private constructor( /** Unique session identifier */ fun id(): Optional = Optional.ofNullable(id) - /** ISO timestamp when request was sent */ - fun xSentAt(): Optional = Optional.ofNullable(xSentAt) - /** Whether to stream the response via SSE */ fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) @@ -123,7 +128,6 @@ private constructor( class Builder internal constructor() { private var id: String? = null - private var xSentAt: OffsetDateTime? = null private var xStreamResponse: XStreamResponse? = null private var body: Body.Builder = Body.builder() private var additionalHeaders: Headers.Builder = Headers.builder() @@ -132,7 +136,6 @@ private constructor( @JvmSynthetic internal fun from(sessionExtractParams: SessionExtractParams) = apply { id = sessionExtractParams.id - xSentAt = sessionExtractParams.xSentAt xStreamResponse = sessionExtractParams.xStreamResponse body = sessionExtractParams.body.toBuilder() additionalHeaders = sessionExtractParams.additionalHeaders.toBuilder() @@ -145,12 +148,6 @@ private constructor( /** Alias for calling [Builder.id] with `id.orElse(null)`. */ fun id(id: Optional) = id(id.getOrNull()) - /** ISO timestamp when request was sent */ - fun xSentAt(xSentAt: OffsetDateTime?) = apply { this.xSentAt = xSentAt } - - /** Alias for calling [Builder.xSentAt] with `xSentAt.orElse(null)`. */ - fun xSentAt(xSentAt: Optional) = xSentAt(xSentAt.getOrNull()) - /** Whether to stream the response via SSE */ fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { this.xStreamResponse = xStreamResponse @@ -173,7 +170,10 @@ private constructor( fun body(body: Body) = apply { this.body = body.toBuilder() } /** Target frame ID for the extraction */ - fun frameId(frameId: String) = apply { body.frameId(frameId) } + fun frameId(frameId: String?) = apply { body.frameId(frameId) } + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) /** * Sets [Builder.frameId] to an arbitrary JSON value. @@ -341,7 +341,6 @@ private constructor( fun build(): SessionExtractParams = SessionExtractParams( id, - xSentAt, xStreamResponse, body.build(), additionalHeaders.build(), @@ -360,7 +359,6 @@ private constructor( override fun _headers(): Headers = Headers.builder() .apply { - xSentAt?.let { put("x-sent-at", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(it)) } xStreamResponse?.let { put("x-stream-response", it.toString()) } putAll(additionalHeaders) } @@ -485,7 +483,10 @@ private constructor( } /** Target frame ID for the extraction */ - fun frameId(frameId: String) = frameId(JsonField.of(frameId)) + fun frameId(frameId: String?) = frameId(JsonField.ofNullable(frameId)) + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) /** * Sets [Builder.frameId] to an arbitrary JSON value. @@ -622,7 +623,7 @@ private constructor( class Options @JsonCreator(mode = JsonCreator.Mode.DISABLED) private constructor( - private val model: JsonField, + private val model: JsonField, private val selector: JsonField, private val timeout: JsonField, private val additionalProperties: MutableMap, @@ -630,7 +631,7 @@ private constructor( @JsonCreator private constructor( - @JsonProperty("model") @ExcludeMissing model: JsonField = JsonMissing.of(), + @JsonProperty("model") @ExcludeMissing model: JsonField = JsonMissing.of(), @JsonProperty("selector") @ExcludeMissing selector: JsonField = JsonMissing.of(), @@ -638,13 +639,12 @@ private constructor( ) : this(model, selector, timeout, mutableMapOf()) /** - * Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - * 'anthropic/claude-4.5-opus') + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') * * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if * the server responded with an unexpected value). */ - fun model(): Optional = model.getOptional("model") + fun model(): Optional = model.getOptional("model") /** * CSS selector to scope extraction to a specific element @@ -667,7 +667,7 @@ private constructor( * * Unlike [model], this method doesn't throw if the JSON field has an unexpected type. */ - @JsonProperty("model") @ExcludeMissing fun _model(): JsonField = model + @JsonProperty("model") @ExcludeMissing fun _model(): JsonField = model /** * Returns the raw JSON value of [selector]. @@ -704,7 +704,7 @@ private constructor( /** A builder for [Options]. */ class Builder internal constructor() { - private var model: JsonField = JsonMissing.of() + private var model: JsonField = JsonMissing.of() private var selector: JsonField = JsonMissing.of() private var timeout: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @@ -717,29 +717,23 @@ private constructor( additionalProperties = options.additionalProperties.toMutableMap() } - /** - * Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - * 'anthropic/claude-4.5-opus') - */ - fun model(model: ModelConfig) = model(JsonField.of(model)) + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + fun model(model: Model) = model(JsonField.of(model)) /** * Sets [Builder.model] to an arbitrary JSON value. * - * You should usually call [Builder.model] with a well-typed [ModelConfig] value - * instead. This method is primarily for setting the field to an undocumented or not yet - * supported value. + * You should usually call [Builder.model] with a well-typed [Model] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. */ - fun model(model: JsonField) = apply { this.model = model } + fun model(model: JsonField) = apply { this.model = model } - /** Alias for calling [model] with `ModelConfig.ofName(name)`. */ - fun model(name: String) = model(ModelConfig.ofName(name)) + /** Alias for calling [model] with `Model.ofConfig(config)`. */ + fun model(config: ModelConfig) = model(Model.ofConfig(config)) - /** - * Alias for calling [model] with `ModelConfig.ofModelConfigObject(modelConfigObject)`. - */ - fun model(modelConfigObject: ModelConfig.ModelConfigObject) = - model(ModelConfig.ofModelConfigObject(modelConfigObject)) + /** Alias for calling [model] with `Model.ofString(string)`. */ + fun model(string: String) = model(Model.ofString(string)) /** CSS selector to scope extraction to a specific element */ fun selector(selector: String) = selector(JsonField.of(selector)) @@ -826,6 +820,178 @@ private constructor( (if (selector.asKnown().isPresent) 1 else 0) + (if (timeout.asKnown().isPresent) 1 else 0) + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + @JsonDeserialize(using = Model.Deserializer::class) + @JsonSerialize(using = Model.Serializer::class) + class Model + private constructor( + private val config: ModelConfig? = null, + private val string: String? = null, + private val _json: JsonValue? = null, + ) { + + fun config(): Optional = Optional.ofNullable(config) + + fun string(): Optional = Optional.ofNullable(string) + + fun isConfig(): Boolean = config != null + + fun isString(): Boolean = string != null + + fun asConfig(): ModelConfig = config.getOrThrow("config") + + fun asString(): String = string.getOrThrow("string") + + fun _json(): Optional = Optional.ofNullable(_json) + + fun accept(visitor: Visitor): T = + when { + config != null -> visitor.visitConfig(config) + string != null -> visitor.visitString(string) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + fun validate(): Model = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) { + config.validate() + } + + override fun visitString(string: String) {} + } + ) + 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 = + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) = config.validity() + + override fun visitString(string: String) = 1 + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Model && config == other.config && string == other.string + } + + override fun hashCode(): Int = Objects.hash(config, string) + + override fun toString(): String = + when { + config != null -> "Model{config=$config}" + string != null -> "Model{string=$string}" + _json != null -> "Model{_unknown=$_json}" + else -> throw IllegalStateException("Invalid Model") + } + + companion object { + + @JvmStatic fun ofConfig(config: ModelConfig) = Model(config = config) + + @JvmStatic fun ofString(string: String) = Model(string = string) + } + + /** + * An interface that defines how to map each variant of [Model] to a value of type [T]. + */ + interface Visitor { + + fun visitConfig(config: ModelConfig): T + + fun visitString(string: String): T + + /** + * Maps an unknown variant of [Model] to a value of type [T]. + * + * An instance of [Model] can contain an unknown variant if it was deserialized from + * data that doesn't match any known variant. For example, if the SDK is on an older + * version than the API, then the API may respond with new variants that the SDK is + * unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown Model: $json") + } + } + + internal class Deserializer : BaseDeserializer(Model::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): Model { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + Model(config = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef())?.let { + Model(string = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely incompatible + // with all the possible variants (e.g. deserializing from boolean). + 0 -> Model(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use the + // first completely valid match, or simply the first match if none are + // completely valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : BaseSerializer(Model::class) { + + override fun serialize( + value: Model, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.config != null -> generator.writeObject(value.config) + value.string != null -> generator.writeObject(value.string) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid Model") + } + } + } + } + override fun equals(other: Any?): Boolean { if (this === other) { return true @@ -1087,7 +1253,6 @@ private constructor( return other is SessionExtractParams && id == other.id && - xSentAt == other.xSentAt && xStreamResponse == other.xStreamResponse && body == other.body && additionalHeaders == other.additionalHeaders && @@ -1095,8 +1260,8 @@ private constructor( } override fun hashCode(): Int = - Objects.hash(id, xSentAt, xStreamResponse, body, additionalHeaders, additionalQueryParams) + Objects.hash(id, xStreamResponse, body, additionalHeaders, additionalQueryParams) override fun toString() = - "SessionExtractParams{id=$id, xSentAt=$xSentAt, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" + "SessionExtractParams{id=$id, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractResponse.kt index e281560..306abcd 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractResponse.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionExtractResponse.kt @@ -203,7 +203,14 @@ private constructor( @JsonProperty("actionId") @ExcludeMissing actionId: JsonField = JsonMissing.of(), ) : this(result, actionId, mutableMapOf()) - /** Extracted data matching the requested schema */ + /** + * Extracted data matching the requested schema + * + * This arbitrary value can be deserialized into a custom type using the `convert` method: + * ```java + * MyClass myObject = data.result().convert(MyClass.class); + * ``` + */ @JsonProperty("result") @ExcludeMissing fun _result(): JsonValue = result /** diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateParams.kt index 2d63849..d848600 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateParams.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateParams.kt @@ -16,8 +16,6 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter import com.fasterxml.jackson.annotation.JsonAnySetter import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter import java.util.Collections import java.util.Objects import java.util.Optional @@ -27,7 +25,6 @@ import kotlin.jvm.optionals.getOrNull class SessionNavigateParams private constructor( private val id: String?, - private val xSentAt: OffsetDateTime?, private val xStreamResponse: XStreamResponse?, private val body: Body, private val additionalHeaders: Headers, @@ -37,9 +34,6 @@ private constructor( /** Unique session identifier */ fun id(): Optional = Optional.ofNullable(id) - /** ISO timestamp when request was sent */ - fun xSentAt(): Optional = Optional.ofNullable(xSentAt) - /** Whether to stream the response via SSE */ fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) @@ -128,7 +122,6 @@ private constructor( class Builder internal constructor() { private var id: String? = null - private var xSentAt: OffsetDateTime? = null private var xStreamResponse: XStreamResponse? = null private var body: Body.Builder = Body.builder() private var additionalHeaders: Headers.Builder = Headers.builder() @@ -137,7 +130,6 @@ private constructor( @JvmSynthetic internal fun from(sessionNavigateParams: SessionNavigateParams) = apply { id = sessionNavigateParams.id - xSentAt = sessionNavigateParams.xSentAt xStreamResponse = sessionNavigateParams.xStreamResponse body = sessionNavigateParams.body.toBuilder() additionalHeaders = sessionNavigateParams.additionalHeaders.toBuilder() @@ -150,12 +142,6 @@ private constructor( /** Alias for calling [Builder.id] with `id.orElse(null)`. */ fun id(id: Optional) = id(id.getOrNull()) - /** ISO timestamp when request was sent */ - fun xSentAt(xSentAt: OffsetDateTime?) = apply { this.xSentAt = xSentAt } - - /** Alias for calling [Builder.xSentAt] with `xSentAt.orElse(null)`. */ - fun xSentAt(xSentAt: Optional) = xSentAt(xSentAt.getOrNull()) - /** Whether to stream the response via SSE */ fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { this.xStreamResponse = xStreamResponse @@ -189,7 +175,10 @@ private constructor( fun url(url: JsonField) = apply { body.url(url) } /** Target frame ID for the navigation */ - fun frameId(frameId: String) = apply { body.frameId(frameId) } + fun frameId(frameId: String?) = apply { body.frameId(frameId) } + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) /** * Sets [Builder.frameId] to an arbitrary JSON value. @@ -355,7 +344,6 @@ private constructor( fun build(): SessionNavigateParams = SessionNavigateParams( id, - xSentAt, xStreamResponse, body.build(), additionalHeaders.build(), @@ -374,7 +362,6 @@ private constructor( override fun _headers(): Headers = Headers.builder() .apply { - xSentAt?.let { put("x-sent-at", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(it)) } xStreamResponse?.let { put("x-stream-response", it.toString()) } putAll(additionalHeaders) } @@ -519,7 +506,10 @@ private constructor( fun url(url: JsonField) = apply { this.url = url } /** Target frame ID for the navigation */ - fun frameId(frameId: String) = frameId(JsonField.of(frameId)) + fun frameId(frameId: String?) = frameId(JsonField.ofNullable(frameId)) + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) /** * Sets [Builder.frameId] to an arbitrary JSON value. @@ -1150,7 +1140,6 @@ private constructor( return other is SessionNavigateParams && id == other.id && - xSentAt == other.xSentAt && xStreamResponse == other.xStreamResponse && body == other.body && additionalHeaders == other.additionalHeaders && @@ -1158,8 +1147,8 @@ private constructor( } override fun hashCode(): Int = - Objects.hash(id, xSentAt, xStreamResponse, body, additionalHeaders, additionalQueryParams) + Objects.hash(id, xStreamResponse, body, additionalHeaders, additionalQueryParams) override fun toString() = - "SessionNavigateParams{id=$id, xSentAt=$xSentAt, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" + "SessionNavigateParams{id=$id, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateResponse.kt index 2518477..1f6e688 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateResponse.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionNavigateResponse.kt @@ -203,7 +203,14 @@ private constructor( @JsonProperty("actionId") @ExcludeMissing actionId: JsonField = JsonMissing.of(), ) : this(result, actionId, mutableMapOf()) - /** Navigation response (Playwright Response object or null) */ + /** + * Navigation response (Playwright Response object or null) + * + * This arbitrary value can be deserialized into a custom type using the `convert` method: + * ```java + * MyClass myObject = data.result().convert(MyClass.class); + * ``` + */ @JsonProperty("result") @ExcludeMissing fun _result(): JsonValue = result /** 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 37efafd..695025f 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 @@ -2,12 +2,16 @@ package com.browserbase.api.models.sessions +import com.browserbase.api.core.BaseDeserializer +import com.browserbase.api.core.BaseSerializer import com.browserbase.api.core.Enum import com.browserbase.api.core.ExcludeMissing import com.browserbase.api.core.JsonField import com.browserbase.api.core.JsonMissing import com.browserbase.api.core.JsonValue import com.browserbase.api.core.Params +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.errors.StagehandInvalidDataException @@ -15,8 +19,13 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter import com.fasterxml.jackson.annotation.JsonAnySetter import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.ObjectCodec +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef import java.util.Collections import java.util.Objects import java.util.Optional @@ -28,7 +37,6 @@ import kotlin.jvm.optionals.getOrNull class SessionObserveParams private constructor( private val id: String?, - private val xSentAt: OffsetDateTime?, private val xStreamResponse: XStreamResponse?, private val body: Body, private val additionalHeaders: Headers, @@ -38,9 +46,6 @@ private constructor( /** Unique session identifier */ fun id(): Optional = Optional.ofNullable(id) - /** ISO timestamp when request was sent */ - fun xSentAt(): Optional = Optional.ofNullable(xSentAt) - /** Whether to stream the response via SSE */ fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) @@ -109,7 +114,6 @@ private constructor( class Builder internal constructor() { private var id: String? = null - private var xSentAt: OffsetDateTime? = null private var xStreamResponse: XStreamResponse? = null private var body: Body.Builder = Body.builder() private var additionalHeaders: Headers.Builder = Headers.builder() @@ -118,7 +122,6 @@ private constructor( @JvmSynthetic internal fun from(sessionObserveParams: SessionObserveParams) = apply { id = sessionObserveParams.id - xSentAt = sessionObserveParams.xSentAt xStreamResponse = sessionObserveParams.xStreamResponse body = sessionObserveParams.body.toBuilder() additionalHeaders = sessionObserveParams.additionalHeaders.toBuilder() @@ -131,12 +134,6 @@ private constructor( /** Alias for calling [Builder.id] with `id.orElse(null)`. */ fun id(id: Optional) = id(id.getOrNull()) - /** ISO timestamp when request was sent */ - fun xSentAt(xSentAt: OffsetDateTime?) = apply { this.xSentAt = xSentAt } - - /** Alias for calling [Builder.xSentAt] with `xSentAt.orElse(null)`. */ - fun xSentAt(xSentAt: Optional) = xSentAt(xSentAt.getOrNull()) - /** Whether to stream the response via SSE */ fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { this.xStreamResponse = xStreamResponse @@ -158,7 +155,10 @@ private constructor( fun body(body: Body) = apply { this.body = body.toBuilder() } /** Target frame ID for the observation */ - fun frameId(frameId: String) = apply { body.frameId(frameId) } + fun frameId(frameId: String?) = apply { body.frameId(frameId) } + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) /** * Sets [Builder.frameId] to an arbitrary JSON value. @@ -315,7 +315,6 @@ private constructor( fun build(): SessionObserveParams = SessionObserveParams( id, - xSentAt, xStreamResponse, body.build(), additionalHeaders.build(), @@ -334,7 +333,6 @@ private constructor( override fun _headers(): Headers = Headers.builder() .apply { - xSentAt?.let { put("x-sent-at", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(it)) } xStreamResponse?.let { put("x-stream-response", it.toString()) } putAll(additionalHeaders) } @@ -440,7 +438,10 @@ private constructor( } /** Target frame ID for the observation */ - fun frameId(frameId: String) = frameId(JsonField.of(frameId)) + fun frameId(frameId: String?) = frameId(JsonField.ofNullable(frameId)) + + /** Alias for calling [Builder.frameId] with `frameId.orElse(null)`. */ + fun frameId(frameId: Optional) = frameId(frameId.getOrNull()) /** * Sets [Builder.frameId] to an arbitrary JSON value. @@ -562,7 +563,7 @@ private constructor( class Options @JsonCreator(mode = JsonCreator.Mode.DISABLED) private constructor( - private val model: JsonField, + private val model: JsonField, private val selector: JsonField, private val timeout: JsonField, private val additionalProperties: MutableMap, @@ -570,7 +571,7 @@ private constructor( @JsonCreator private constructor( - @JsonProperty("model") @ExcludeMissing model: JsonField = JsonMissing.of(), + @JsonProperty("model") @ExcludeMissing model: JsonField = JsonMissing.of(), @JsonProperty("selector") @ExcludeMissing selector: JsonField = JsonMissing.of(), @@ -578,13 +579,12 @@ private constructor( ) : this(model, selector, timeout, mutableMapOf()) /** - * Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - * 'anthropic/claude-4.5-opus') + * Model configuration object or model name string (e.g., 'openai/gpt-5-nano') * * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if * the server responded with an unexpected value). */ - fun model(): Optional = model.getOptional("model") + fun model(): Optional = model.getOptional("model") /** * CSS selector to scope observation to a specific element @@ -607,7 +607,7 @@ private constructor( * * Unlike [model], this method doesn't throw if the JSON field has an unexpected type. */ - @JsonProperty("model") @ExcludeMissing fun _model(): JsonField = model + @JsonProperty("model") @ExcludeMissing fun _model(): JsonField = model /** * Returns the raw JSON value of [selector]. @@ -644,7 +644,7 @@ private constructor( /** A builder for [Options]. */ class Builder internal constructor() { - private var model: JsonField = JsonMissing.of() + private var model: JsonField = JsonMissing.of() private var selector: JsonField = JsonMissing.of() private var timeout: JsonField = JsonMissing.of() private var additionalProperties: MutableMap = mutableMapOf() @@ -657,29 +657,23 @@ private constructor( additionalProperties = options.additionalProperties.toMutableMap() } - /** - * Model name string with provider prefix (e.g., 'openai/gpt-5-nano', - * 'anthropic/claude-4.5-opus') - */ - fun model(model: ModelConfig) = model(JsonField.of(model)) + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + fun model(model: Model) = model(JsonField.of(model)) /** * Sets [Builder.model] to an arbitrary JSON value. * - * You should usually call [Builder.model] with a well-typed [ModelConfig] value - * instead. This method is primarily for setting the field to an undocumented or not yet - * supported value. + * You should usually call [Builder.model] with a well-typed [Model] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported + * value. */ - fun model(model: JsonField) = apply { this.model = model } + fun model(model: JsonField) = apply { this.model = model } - /** Alias for calling [model] with `ModelConfig.ofName(name)`. */ - fun model(name: String) = model(ModelConfig.ofName(name)) + /** Alias for calling [model] with `Model.ofConfig(config)`. */ + fun model(config: ModelConfig) = model(Model.ofConfig(config)) - /** - * Alias for calling [model] with `ModelConfig.ofModelConfigObject(modelConfigObject)`. - */ - fun model(modelConfigObject: ModelConfig.ModelConfigObject) = - model(ModelConfig.ofModelConfigObject(modelConfigObject)) + /** Alias for calling [model] with `Model.ofString(string)`. */ + fun model(string: String) = model(Model.ofString(string)) /** CSS selector to scope observation to a specific element */ fun selector(selector: String) = selector(JsonField.of(selector)) @@ -766,6 +760,178 @@ private constructor( (if (selector.asKnown().isPresent) 1 else 0) + (if (timeout.asKnown().isPresent) 1 else 0) + /** Model configuration object or model name string (e.g., 'openai/gpt-5-nano') */ + @JsonDeserialize(using = Model.Deserializer::class) + @JsonSerialize(using = Model.Serializer::class) + class Model + private constructor( + private val config: ModelConfig? = null, + private val string: String? = null, + private val _json: JsonValue? = null, + ) { + + fun config(): Optional = Optional.ofNullable(config) + + fun string(): Optional = Optional.ofNullable(string) + + fun isConfig(): Boolean = config != null + + fun isString(): Boolean = string != null + + fun asConfig(): ModelConfig = config.getOrThrow("config") + + fun asString(): String = string.getOrThrow("string") + + fun _json(): Optional = Optional.ofNullable(_json) + + fun accept(visitor: Visitor): T = + when { + config != null -> visitor.visitConfig(config) + string != null -> visitor.visitString(string) + else -> visitor.unknown(_json) + } + + private var validated: Boolean = false + + fun validate(): Model = apply { + if (validated) { + return@apply + } + + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) { + config.validate() + } + + override fun visitString(string: String) {} + } + ) + 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 = + accept( + object : Visitor { + override fun visitConfig(config: ModelConfig) = config.validity() + + override fun visitString(string: String) = 1 + + override fun unknown(json: JsonValue?) = 0 + } + ) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Model && config == other.config && string == other.string + } + + override fun hashCode(): Int = Objects.hash(config, string) + + override fun toString(): String = + when { + config != null -> "Model{config=$config}" + string != null -> "Model{string=$string}" + _json != null -> "Model{_unknown=$_json}" + else -> throw IllegalStateException("Invalid Model") + } + + companion object { + + @JvmStatic fun ofConfig(config: ModelConfig) = Model(config = config) + + @JvmStatic fun ofString(string: String) = Model(string = string) + } + + /** + * An interface that defines how to map each variant of [Model] to a value of type [T]. + */ + interface Visitor { + + fun visitConfig(config: ModelConfig): T + + fun visitString(string: String): T + + /** + * Maps an unknown variant of [Model] to a value of type [T]. + * + * An instance of [Model] can contain an unknown variant if it was deserialized from + * data that doesn't match any known variant. For example, if the SDK is on an older + * version than the API, then the API may respond with new variants that the SDK is + * unaware of. + * + * @throws StagehandInvalidDataException in the default implementation. + */ + fun unknown(json: JsonValue?): T { + throw StagehandInvalidDataException("Unknown Model: $json") + } + } + + internal class Deserializer : BaseDeserializer(Model::class) { + + override fun ObjectCodec.deserialize(node: JsonNode): Model { + val json = JsonValue.fromJsonNode(node) + + val bestMatches = + sequenceOf( + tryDeserialize(node, jacksonTypeRef())?.let { + Model(config = it, _json = json) + }, + tryDeserialize(node, jacksonTypeRef())?.let { + Model(string = it, _json = json) + }, + ) + .filterNotNull() + .allMaxBy { it.validity() } + .toList() + return when (bestMatches.size) { + // This can happen if what we're deserializing is completely incompatible + // with all the possible variants (e.g. deserializing from boolean). + 0 -> Model(_json = json) + 1 -> bestMatches.single() + // If there's more than one match with the highest validity, then use the + // first completely valid match, or simply the first match if none are + // completely valid. + else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first() + } + } + } + + internal class Serializer : BaseSerializer(Model::class) { + + override fun serialize( + value: Model, + generator: JsonGenerator, + provider: SerializerProvider, + ) { + when { + value.config != null -> generator.writeObject(value.config) + value.string != null -> generator.writeObject(value.string) + value._json != null -> generator.writeObject(value._json) + else -> throw IllegalStateException("Invalid Model") + } + } + } + } + override fun equals(other: Any?): Boolean { if (this === other) { return true @@ -927,7 +1093,6 @@ private constructor( return other is SessionObserveParams && id == other.id && - xSentAt == other.xSentAt && xStreamResponse == other.xStreamResponse && body == other.body && additionalHeaders == other.additionalHeaders && @@ -935,8 +1100,8 @@ private constructor( } override fun hashCode(): Int = - Objects.hash(id, xSentAt, xStreamResponse, body, additionalHeaders, additionalQueryParams) + Objects.hash(id, xStreamResponse, body, additionalHeaders, additionalQueryParams) override fun toString() = - "SessionObserveParams{id=$id, xSentAt=$xSentAt, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" + "SessionObserveParams{id=$id, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionReplayParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionReplayParams.kt new file mode 100644 index 0000000..093a776 --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionReplayParams.kt @@ -0,0 +1,355 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.Enum +import com.browserbase.api.core.JsonField +import com.browserbase.api.core.Params +import com.browserbase.api.core.http.Headers +import com.browserbase.api.core.http.QueryParams +import com.browserbase.api.errors.StagehandInvalidDataException +import com.fasterxml.jackson.annotation.JsonCreator +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +/** Retrieves replay metrics for a session. */ +class SessionReplayParams +private constructor( + private val id: String?, + private val xStreamResponse: XStreamResponse?, + private val additionalHeaders: Headers, + private val additionalQueryParams: QueryParams, +) : Params { + + /** Unique session identifier */ + fun id(): Optional = Optional.ofNullable(id) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) + + /** Additional headers to send with the request. */ + fun _additionalHeaders(): Headers = additionalHeaders + + /** Additional query param to send with the request. */ + fun _additionalQueryParams(): QueryParams = additionalQueryParams + + fun toBuilder() = Builder().from(this) + + companion object { + + @JvmStatic fun none(): SessionReplayParams = builder().build() + + /** Returns a mutable builder for constructing an instance of [SessionReplayParams]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionReplayParams]. */ + class Builder internal constructor() { + + private var id: String? = null + private var xStreamResponse: XStreamResponse? = null + private var additionalHeaders: Headers.Builder = Headers.builder() + private var additionalQueryParams: QueryParams.Builder = QueryParams.builder() + + @JvmSynthetic + internal fun from(sessionReplayParams: SessionReplayParams) = apply { + id = sessionReplayParams.id + xStreamResponse = sessionReplayParams.xStreamResponse + additionalHeaders = sessionReplayParams.additionalHeaders.toBuilder() + additionalQueryParams = sessionReplayParams.additionalQueryParams.toBuilder() + } + + /** Unique session identifier */ + fun id(id: String?) = apply { this.id = id } + + /** Alias for calling [Builder.id] with `id.orElse(null)`. */ + fun id(id: Optional) = id(id.getOrNull()) + + /** Whether to stream the response via SSE */ + fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { + this.xStreamResponse = xStreamResponse + } + + /** Alias for calling [Builder.xStreamResponse] with `xStreamResponse.orElse(null)`. */ + fun xStreamResponse(xStreamResponse: Optional) = + xStreamResponse(xStreamResponse.getOrNull()) + + fun additionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun additionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.clear() + putAllAdditionalHeaders(additionalHeaders) + } + + fun putAdditionalHeader(name: String, value: String) = apply { + additionalHeaders.put(name, value) + } + + fun putAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.put(name, values) + } + + fun putAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun putAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.putAll(additionalHeaders) + } + + fun replaceAdditionalHeaders(name: String, value: String) = apply { + additionalHeaders.replace(name, value) + } + + fun replaceAdditionalHeaders(name: String, values: Iterable) = apply { + additionalHeaders.replace(name, values) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Headers) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun replaceAllAdditionalHeaders(additionalHeaders: Map>) = apply { + this.additionalHeaders.replaceAll(additionalHeaders) + } + + fun removeAdditionalHeaders(name: String) = apply { additionalHeaders.remove(name) } + + fun removeAllAdditionalHeaders(names: Set) = apply { + additionalHeaders.removeAll(names) + } + + fun additionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun additionalQueryParams(additionalQueryParams: Map>) = apply { + this.additionalQueryParams.clear() + putAllAdditionalQueryParams(additionalQueryParams) + } + + fun putAdditionalQueryParam(key: String, value: String) = apply { + additionalQueryParams.put(key, value) + } + + fun putAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.put(key, values) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun putAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.putAll(additionalQueryParams) + } + + fun replaceAdditionalQueryParams(key: String, value: String) = apply { + additionalQueryParams.replace(key, value) + } + + fun replaceAdditionalQueryParams(key: String, values: Iterable) = apply { + additionalQueryParams.replace(key, values) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun replaceAllAdditionalQueryParams(additionalQueryParams: Map>) = + apply { + this.additionalQueryParams.replaceAll(additionalQueryParams) + } + + fun removeAdditionalQueryParams(key: String) = apply { additionalQueryParams.remove(key) } + + fun removeAllAdditionalQueryParams(keys: Set) = apply { + additionalQueryParams.removeAll(keys) + } + + /** + * Returns an immutable instance of [SessionReplayParams]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): SessionReplayParams = + SessionReplayParams( + id, + xStreamResponse, + additionalHeaders.build(), + additionalQueryParams.build(), + ) + } + + fun _pathParam(index: Int): String = + when (index) { + 0 -> id ?: "" + else -> "" + } + + override fun _headers(): Headers = + Headers.builder() + .apply { + xStreamResponse?.let { put("x-stream-response", it.toString()) } + putAll(additionalHeaders) + } + .build() + + override fun _queryParams(): QueryParams = additionalQueryParams + + /** Whether to stream the response via SSE */ + class XStreamResponse @JsonCreator private constructor(private val value: JsonField) : + Enum { + + /** + * Returns this class instance's raw value. + * + * This is usually only useful if this instance was deserialized from data that doesn't + * match any known member, and you want to know that value. For example, if the SDK is on an + * older version than the API, then the API may respond with new members that the SDK is + * unaware of. + */ + @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value + + companion object { + + @JvmField val TRUE = of("true") + + @JvmField val FALSE = of("false") + + @JvmStatic fun of(value: String) = XStreamResponse(JsonField.of(value)) + } + + /** An enum containing [XStreamResponse]'s known values. */ + enum class Known { + TRUE, + FALSE, + } + + /** + * An enum containing [XStreamResponse]'s known values, as well as an [_UNKNOWN] member. + * + * An instance of [XStreamResponse] can contain an unknown value in a couple of cases: + * - It was deserialized from data that doesn't match any known member. For example, if the + * SDK is on an older version than the API, then the API may respond with new members that + * the SDK is unaware of. + * - It was constructed with an arbitrary value using the [of] method. + */ + enum class Value { + TRUE, + FALSE, + /** + * An enum member indicating that [XStreamResponse] was instantiated with an unknown + * value. + */ + _UNKNOWN, + } + + /** + * Returns an enum member corresponding to this class instance's value, or [Value._UNKNOWN] + * if the class was instantiated with an unknown value. + * + * Use the [known] method instead if you're certain the value is always known or if you want + * to throw for the unknown case. + */ + fun value(): Value = + when (this) { + TRUE -> Value.TRUE + FALSE -> Value.FALSE + else -> Value._UNKNOWN + } + + /** + * Returns an enum member corresponding to this class instance's value. + * + * Use the [value] method instead if you're uncertain the value is always known and don't + * want to throw for the unknown case. + * + * @throws StagehandInvalidDataException if this class instance's value is a not a known + * member. + */ + fun known(): Known = + when (this) { + TRUE -> Known.TRUE + FALSE -> Known.FALSE + else -> throw StagehandInvalidDataException("Unknown XStreamResponse: $value") + } + + /** + * Returns this class instance's primitive wire representation. + * + * This differs from the [toString] method because that method is primarily for debugging + * and generally doesn't throw. + * + * @throws StagehandInvalidDataException if this class instance's value does not have the + * expected primitive type. + */ + fun asString(): String = + _value().asString().orElseThrow { + StagehandInvalidDataException("Value is not a String") + } + + private var validated: Boolean = false + + fun validate(): XStreamResponse = apply { + if (validated) { + return@apply + } + + known() + 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 = if (value() == Value._UNKNOWN) 0 else 1 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is XStreamResponse && value == other.value + } + + override fun hashCode() = value.hashCode() + + override fun toString() = value.toString() + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionReplayParams && + id == other.id && + xStreamResponse == other.xStreamResponse && + additionalHeaders == other.additionalHeaders && + additionalQueryParams == other.additionalQueryParams + } + + override fun hashCode(): Int = + Objects.hash(id, xStreamResponse, additionalHeaders, additionalQueryParams) + + override fun toString() = + "SessionReplayParams{id=$id, xStreamResponse=$xStreamResponse, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" +} diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionReplayResponse.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionReplayResponse.kt new file mode 100644 index 0000000..39ecafa --- /dev/null +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionReplayResponse.kt @@ -0,0 +1,1053 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.ExcludeMissing +import com.browserbase.api.core.JsonField +import com.browserbase.api.core.JsonMissing +import com.browserbase.api.core.JsonValue +import com.browserbase.api.core.checkKnown +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 +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import java.util.Collections +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +class SessionReplayResponse +@JsonCreator(mode = JsonCreator.Mode.DISABLED) +private constructor( + private val data: JsonField, + private val success: JsonField, + private val additionalProperties: MutableMap, +) { + + @JsonCreator + private constructor( + @JsonProperty("data") @ExcludeMissing data: JsonField = JsonMissing.of(), + @JsonProperty("success") @ExcludeMissing success: JsonField = JsonMissing.of(), + ) : this(data, success, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun data(): Data = data.getRequired("data") + + /** + * Indicates whether the request was successful + * + * @throws StagehandInvalidDataException if the JSON field has an unexpected type or is + * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + */ + fun success(): Boolean = success.getRequired("success") + + /** + * Returns the raw JSON value of [data]. + * + * Unlike [data], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("data") @ExcludeMissing fun _data(): JsonField = data + + /** + * Returns the raw JSON value of [success]. + * + * Unlike [success], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("success") @ExcludeMissing fun _success(): JsonField = success + + @JsonAnySetter + private fun putAdditionalProperty(key: String, value: JsonValue) { + additionalProperties.put(key, value) + } + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = + Collections.unmodifiableMap(additionalProperties) + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [SessionReplayResponse]. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [SessionReplayResponse]. */ + class Builder internal constructor() { + + private var data: JsonField? = null + private var success: JsonField? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(sessionReplayResponse: SessionReplayResponse) = apply { + data = sessionReplayResponse.data + success = sessionReplayResponse.success + additionalProperties = sessionReplayResponse.additionalProperties.toMutableMap() + } + + fun data(data: Data) = data(JsonField.of(data)) + + /** + * Sets [Builder.data] to an arbitrary JSON value. + * + * You should usually call [Builder.data] with a well-typed [Data] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun data(data: JsonField) = apply { this.data = data } + + /** Indicates whether the request was successful */ + fun success(success: Boolean) = success(JsonField.of(success)) + + /** + * Sets [Builder.success] to an arbitrary JSON value. + * + * You should usually call [Builder.success] with a well-typed [Boolean] value instead. This + * method is primarily for setting the field to an undocumented or not yet supported value. + */ + fun success(success: JsonField) = apply { this.success = success } + + 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 [SessionReplayResponse]. + * + * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .data() + * .success() + * ``` + * + * @throws IllegalStateException if any required field is unset. + */ + fun build(): SessionReplayResponse = + SessionReplayResponse( + checkRequired("data", data), + checkRequired("success", success), + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + fun validate(): SessionReplayResponse = apply { + if (validated) { + return@apply + } + + data().validate() + success() + 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 = + (data.asKnown().getOrNull()?.validity() ?: 0) + (if (success.asKnown().isPresent) 1 else 0) + + class Data + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val pages: JsonField>, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("pages") @ExcludeMissing pages: JsonField> = JsonMissing.of() + ) : this(pages, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. if + * the server responded with an unexpected value). + */ + fun pages(): Optional> = pages.getOptional("pages") + + /** + * Returns the raw JSON value of [pages]. + * + * Unlike [pages], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("pages") @ExcludeMissing fun _pages(): JsonField> = pages + + @JsonAnySetter + private fun putAdditionalProperty(key: String, value: JsonValue) { + additionalProperties.put(key, value) + } + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = + Collections.unmodifiableMap(additionalProperties) + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [Data]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Data]. */ + class Builder internal constructor() { + + private var pages: JsonField>? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(data: Data) = apply { + pages = data.pages.map { it.toMutableList() } + additionalProperties = data.additionalProperties.toMutableMap() + } + + fun pages(pages: List) = pages(JsonField.of(pages)) + + /** + * Sets [Builder.pages] to an arbitrary JSON value. + * + * You should usually call [Builder.pages] with a well-typed `List` value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun pages(pages: JsonField>) = apply { + this.pages = pages.map { it.toMutableList() } + } + + /** + * Adds a single [Page] to [pages]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addPage(page: Page) = apply { + pages = + (pages ?: JsonField.of(mutableListOf())).also { + checkKnown("pages", it).add(page) + } + } + + 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 [Data]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Data = + Data( + (pages ?: JsonMissing.of()).map { it.toImmutable() }, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + fun validate(): Data = apply { + if (validated) { + return@apply + } + + pages().ifPresent { it.forEach { it.validate() } } + 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 = + (pages.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) + + class Page + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val actions: JsonField>, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("actions") + @ExcludeMissing + actions: JsonField> = JsonMissing.of() + ) : this(actions, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun actions(): Optional> = actions.getOptional("actions") + + /** + * Returns the raw JSON value of [actions]. + * + * Unlike [actions], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("actions") + @ExcludeMissing + fun _actions(): JsonField> = actions + + @JsonAnySetter + private fun putAdditionalProperty(key: String, value: JsonValue) { + additionalProperties.put(key, value) + } + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = + Collections.unmodifiableMap(additionalProperties) + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [Page]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Page]. */ + class Builder internal constructor() { + + private var actions: JsonField>? = null + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(page: Page) = apply { + actions = page.actions.map { it.toMutableList() } + additionalProperties = page.additionalProperties.toMutableMap() + } + + fun actions(actions: List) = actions(JsonField.of(actions)) + + /** + * Sets [Builder.actions] to an arbitrary JSON value. + * + * You should usually call [Builder.actions] with a well-typed `List` value + * instead. This method is primarily for setting the field to an undocumented or not + * yet supported value. + */ + fun actions(actions: JsonField>) = apply { + this.actions = actions.map { it.toMutableList() } + } + + /** + * Adds a single [Action] to [actions]. + * + * @throws IllegalStateException if the field was previously set to a non-list. + */ + fun addAction(action: Action) = apply { + actions = + (actions ?: JsonField.of(mutableListOf())).also { + checkKnown("actions", it).add(action) + } + } + + 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 [Page]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Page = + Page( + (actions ?: JsonMissing.of()).map { it.toImmutable() }, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + fun validate(): Page = apply { + if (validated) { + return@apply + } + + actions().ifPresent { it.forEach { it.validate() } } + 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 = + (actions.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) + + class Action + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val method: JsonField, + private val tokenUsage: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("method") + @ExcludeMissing + method: JsonField = JsonMissing.of(), + @JsonProperty("tokenUsage") + @ExcludeMissing + tokenUsage: JsonField = JsonMissing.of(), + ) : this(method, tokenUsage, mutableMapOf()) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun method(): Optional = method.getOptional("method") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type + * (e.g. if the server responded with an unexpected value). + */ + fun tokenUsage(): Optional = tokenUsage.getOptional("tokenUsage") + + /** + * Returns the raw JSON value of [method]. + * + * Unlike [method], this method doesn't throw if the JSON field has an unexpected + * type. + */ + @JsonProperty("method") @ExcludeMissing fun _method(): JsonField = method + + /** + * Returns the raw JSON value of [tokenUsage]. + * + * Unlike [tokenUsage], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("tokenUsage") + @ExcludeMissing + fun _tokenUsage(): JsonField = tokenUsage + + @JsonAnySetter + private fun putAdditionalProperty(key: String, value: JsonValue) { + additionalProperties.put(key, value) + } + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = + Collections.unmodifiableMap(additionalProperties) + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [Action]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Action]. */ + class Builder internal constructor() { + + private var method: JsonField = JsonMissing.of() + private var tokenUsage: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(action: Action) = apply { + method = action.method + tokenUsage = action.tokenUsage + additionalProperties = action.additionalProperties.toMutableMap() + } + + fun method(method: String) = method(JsonField.of(method)) + + /** + * Sets [Builder.method] to an arbitrary JSON value. + * + * You should usually call [Builder.method] with a well-typed [String] value + * instead. This method is primarily for setting the field to an undocumented or + * not yet supported value. + */ + fun method(method: JsonField) = apply { this.method = method } + + fun tokenUsage(tokenUsage: TokenUsage) = tokenUsage(JsonField.of(tokenUsage)) + + /** + * Sets [Builder.tokenUsage] to an arbitrary JSON value. + * + * You should usually call [Builder.tokenUsage] with a well-typed [TokenUsage] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun tokenUsage(tokenUsage: JsonField) = apply { + this.tokenUsage = tokenUsage + } + + 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 [Action]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Action = + Action(method, tokenUsage, additionalProperties.toMutableMap()) + } + + private var validated: Boolean = false + + fun validate(): Action = apply { + if (validated) { + return@apply + } + + method() + tokenUsage().ifPresent { it.validate() } + 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 = + (if (method.asKnown().isPresent) 1 else 0) + + (tokenUsage.asKnown().getOrNull()?.validity() ?: 0) + + class TokenUsage + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val cachedInputTokens: JsonField, + private val inputTokens: JsonField, + private val outputTokens: JsonField, + private val reasoningTokens: JsonField, + private val timeMs: JsonField, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("cachedInputTokens") + @ExcludeMissing + cachedInputTokens: JsonField = JsonMissing.of(), + @JsonProperty("inputTokens") + @ExcludeMissing + inputTokens: JsonField = JsonMissing.of(), + @JsonProperty("outputTokens") + @ExcludeMissing + outputTokens: JsonField = JsonMissing.of(), + @JsonProperty("reasoningTokens") + @ExcludeMissing + reasoningTokens: JsonField = JsonMissing.of(), + @JsonProperty("timeMs") + @ExcludeMissing + timeMs: JsonField = JsonMissing.of(), + ) : this( + cachedInputTokens, + inputTokens, + outputTokens, + reasoningTokens, + timeMs, + mutableMapOf(), + ) + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun cachedInputTokens(): Optional = + cachedInputTokens.getOptional("cachedInputTokens") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun inputTokens(): Optional = inputTokens.getOptional("inputTokens") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun outputTokens(): Optional = outputTokens.getOptional("outputTokens") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun reasoningTokens(): Optional = + reasoningTokens.getOptional("reasoningTokens") + + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected + * type (e.g. if the server responded with an unexpected value). + */ + fun timeMs(): Optional = timeMs.getOptional("timeMs") + + /** + * Returns the raw JSON value of [cachedInputTokens]. + * + * Unlike [cachedInputTokens], this method doesn't throw if the JSON field has + * an unexpected type. + */ + @JsonProperty("cachedInputTokens") + @ExcludeMissing + fun _cachedInputTokens(): JsonField = cachedInputTokens + + /** + * Returns the raw JSON value of [inputTokens]. + * + * Unlike [inputTokens], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("inputTokens") + @ExcludeMissing + fun _inputTokens(): JsonField = inputTokens + + /** + * Returns the raw JSON value of [outputTokens]. + * + * Unlike [outputTokens], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("outputTokens") + @ExcludeMissing + fun _outputTokens(): JsonField = outputTokens + + /** + * Returns the raw JSON value of [reasoningTokens]. + * + * Unlike [reasoningTokens], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("reasoningTokens") + @ExcludeMissing + fun _reasoningTokens(): JsonField = reasoningTokens + + /** + * Returns the raw JSON value of [timeMs]. + * + * Unlike [timeMs], this method doesn't throw if the JSON field has an + * unexpected type. + */ + @JsonProperty("timeMs") + @ExcludeMissing + fun _timeMs(): JsonField = timeMs + + @JsonAnySetter + private fun putAdditionalProperty(key: String, value: JsonValue) { + additionalProperties.put(key, value) + } + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = + Collections.unmodifiableMap(additionalProperties) + + fun toBuilder() = Builder().from(this) + + companion object { + + /** + * Returns a mutable builder for constructing an instance of [TokenUsage]. + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [TokenUsage]. */ + class Builder internal constructor() { + + private var cachedInputTokens: JsonField = JsonMissing.of() + private var inputTokens: JsonField = JsonMissing.of() + private var outputTokens: JsonField = JsonMissing.of() + private var reasoningTokens: JsonField = JsonMissing.of() + private var timeMs: JsonField = JsonMissing.of() + private var additionalProperties: MutableMap = + mutableMapOf() + + @JvmSynthetic + internal fun from(tokenUsage: TokenUsage) = apply { + cachedInputTokens = tokenUsage.cachedInputTokens + inputTokens = tokenUsage.inputTokens + outputTokens = tokenUsage.outputTokens + reasoningTokens = tokenUsage.reasoningTokens + timeMs = tokenUsage.timeMs + additionalProperties = tokenUsage.additionalProperties.toMutableMap() + } + + fun cachedInputTokens(cachedInputTokens: Double) = + cachedInputTokens(JsonField.of(cachedInputTokens)) + + /** + * Sets [Builder.cachedInputTokens] to an arbitrary JSON value. + * + * You should usually call [Builder.cachedInputTokens] with a well-typed + * [Double] value instead. This method is primarily for setting the field to + * an undocumented or not yet supported value. + */ + fun cachedInputTokens(cachedInputTokens: JsonField) = apply { + this.cachedInputTokens = cachedInputTokens + } + + fun inputTokens(inputTokens: Double) = + inputTokens(JsonField.of(inputTokens)) + + /** + * Sets [Builder.inputTokens] to an arbitrary JSON value. + * + * You should usually call [Builder.inputTokens] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun inputTokens(inputTokens: JsonField) = apply { + this.inputTokens = inputTokens + } + + fun outputTokens(outputTokens: Double) = + outputTokens(JsonField.of(outputTokens)) + + /** + * Sets [Builder.outputTokens] to an arbitrary JSON value. + * + * You should usually call [Builder.outputTokens] with a well-typed [Double] + * value instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun outputTokens(outputTokens: JsonField) = apply { + this.outputTokens = outputTokens + } + + fun reasoningTokens(reasoningTokens: Double) = + reasoningTokens(JsonField.of(reasoningTokens)) + + /** + * Sets [Builder.reasoningTokens] to an arbitrary JSON value. + * + * You should usually call [Builder.reasoningTokens] with a well-typed + * [Double] value instead. This method is primarily for setting the field to + * an undocumented or not yet supported value. + */ + fun reasoningTokens(reasoningTokens: JsonField) = apply { + this.reasoningTokens = reasoningTokens + } + + fun timeMs(timeMs: Double) = timeMs(JsonField.of(timeMs)) + + /** + * Sets [Builder.timeMs] to an arbitrary JSON value. + * + * You should usually call [Builder.timeMs] with a well-typed [Double] value + * instead. This method is primarily for setting the field to an + * undocumented or not yet supported value. + */ + fun timeMs(timeMs: JsonField) = apply { this.timeMs = timeMs } + + 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 [TokenUsage]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): TokenUsage = + TokenUsage( + cachedInputTokens, + inputTokens, + outputTokens, + reasoningTokens, + timeMs, + additionalProperties.toMutableMap(), + ) + } + + private var validated: Boolean = false + + fun validate(): TokenUsage = apply { + if (validated) { + return@apply + } + + cachedInputTokens() + inputTokens() + outputTokens() + reasoningTokens() + timeMs() + 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 = + (if (cachedInputTokens.asKnown().isPresent) 1 else 0) + + (if (inputTokens.asKnown().isPresent) 1 else 0) + + (if (outputTokens.asKnown().isPresent) 1 else 0) + + (if (reasoningTokens.asKnown().isPresent) 1 else 0) + + (if (timeMs.asKnown().isPresent) 1 else 0) + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is TokenUsage && + cachedInputTokens == other.cachedInputTokens && + inputTokens == other.inputTokens && + outputTokens == other.outputTokens && + reasoningTokens == other.reasoningTokens && + timeMs == other.timeMs && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash( + cachedInputTokens, + inputTokens, + outputTokens, + reasoningTokens, + timeMs, + additionalProperties, + ) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "TokenUsage{cachedInputTokens=$cachedInputTokens, inputTokens=$inputTokens, outputTokens=$outputTokens, reasoningTokens=$reasoningTokens, timeMs=$timeMs, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Action && + method == other.method && + tokenUsage == other.tokenUsage && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { + Objects.hash(method, tokenUsage, additionalProperties) + } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Action{method=$method, tokenUsage=$tokenUsage, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Page && + actions == other.actions && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(actions, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Page{actions=$actions, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Data && + pages == other.pages && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(pages, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = "Data{pages=$pages, additionalProperties=$additionalProperties}" + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is SessionReplayResponse && + data == other.data && + success == other.success && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(data, success, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "SessionReplayResponse{data=$data, success=$success, additionalProperties=$additionalProperties}" +} 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 2d3ece2..f177a4e 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 @@ -29,8 +29,6 @@ import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.fasterxml.jackson.module.kotlin.jacksonTypeRef -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter import java.util.Collections import java.util.Objects import java.util.Optional @@ -42,16 +40,12 @@ import kotlin.jvm.optionals.getOrNull */ class SessionStartParams private constructor( - private val xSentAt: OffsetDateTime?, private val xStreamResponse: XStreamResponse?, private val body: Body, private val additionalHeaders: Headers, private val additionalQueryParams: QueryParams, ) : Params { - /** ISO timestamp when request was sent */ - fun xSentAt(): Optional = Optional.ofNullable(xSentAt) - /** Whether to stream the response via SSE */ fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) @@ -246,7 +240,6 @@ private constructor( /** A builder for [SessionStartParams]. */ class Builder internal constructor() { - private var xSentAt: OffsetDateTime? = null private var xStreamResponse: XStreamResponse? = null private var body: Body.Builder = Body.builder() private var additionalHeaders: Headers.Builder = Headers.builder() @@ -254,19 +247,12 @@ private constructor( @JvmSynthetic internal fun from(sessionStartParams: SessionStartParams) = apply { - xSentAt = sessionStartParams.xSentAt xStreamResponse = sessionStartParams.xStreamResponse body = sessionStartParams.body.toBuilder() additionalHeaders = sessionStartParams.additionalHeaders.toBuilder() additionalQueryParams = sessionStartParams.additionalQueryParams.toBuilder() } - /** ISO timestamp when request was sent */ - fun xSentAt(xSentAt: OffsetDateTime?) = apply { this.xSentAt = xSentAt } - - /** Alias for calling [Builder.xSentAt] with `xSentAt.orElse(null)`. */ - fun xSentAt(xSentAt: Optional) = xSentAt(xSentAt.getOrNull()) - /** Whether to stream the response via SSE */ fun xStreamResponse(xStreamResponse: XStreamResponse?) = apply { this.xStreamResponse = xStreamResponse @@ -570,7 +556,6 @@ private constructor( */ fun build(): SessionStartParams = SessionStartParams( - xSentAt, xStreamResponse, body.build(), additionalHeaders.build(), @@ -583,7 +568,6 @@ private constructor( override fun _headers(): Headers = Headers.builder() .apply { - xSentAt?.let { put("x-sent-at", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(it)) } xStreamResponse?.let { put("x-stream-response", it.toString()) } putAll(additionalHeaders) } @@ -1400,6 +1384,7 @@ private constructor( private val ignoreDefaultArgs: JsonField, private val ignoreHttpsErrors: JsonField, private val locale: JsonField, + private val port: JsonField, private val preserveUserDataDir: JsonField, private val proxy: JsonField, private val userDataDir: JsonField, @@ -1451,6 +1436,7 @@ private constructor( @JsonProperty("locale") @ExcludeMissing locale: JsonField = JsonMissing.of(), + @JsonProperty("port") @ExcludeMissing port: JsonField = JsonMissing.of(), @JsonProperty("preserveUserDataDir") @ExcludeMissing preserveUserDataDir: JsonField = JsonMissing.of(), @@ -1476,6 +1462,7 @@ private constructor( ignoreDefaultArgs, ignoreHttpsErrors, locale, + port, preserveUserDataDir, proxy, userDataDir, @@ -1573,6 +1560,12 @@ private constructor( */ fun locale(): Optional = locale.getOptional("locale") + /** + * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. + * if the server responded with an unexpected value). + */ + fun port(): Optional = port.getOptional("port") + /** * @throws StagehandInvalidDataException if the JSON field has an unexpected type (e.g. * if the server responded with an unexpected value). @@ -1723,6 +1716,13 @@ private constructor( */ @JsonProperty("locale") @ExcludeMissing fun _locale(): JsonField = locale + /** + * Returns the raw JSON value of [port]. + * + * Unlike [port], this method doesn't throw if the JSON field has an unexpected type. + */ + @JsonProperty("port") @ExcludeMissing fun _port(): JsonField = port + /** * Returns the raw JSON value of [preserveUserDataDir]. * @@ -1795,6 +1795,7 @@ private constructor( private var ignoreDefaultArgs: JsonField = JsonMissing.of() private var ignoreHttpsErrors: JsonField = JsonMissing.of() private var locale: JsonField = JsonMissing.of() + private var port: JsonField = JsonMissing.of() private var preserveUserDataDir: JsonField = JsonMissing.of() private var proxy: JsonField = JsonMissing.of() private var userDataDir: JsonField = JsonMissing.of() @@ -1817,6 +1818,7 @@ private constructor( ignoreDefaultArgs = launchOptions.ignoreDefaultArgs ignoreHttpsErrors = launchOptions.ignoreHttpsErrors locale = launchOptions.locale + port = launchOptions.port preserveUserDataDir = launchOptions.preserveUserDataDir proxy = launchOptions.proxy userDataDir = launchOptions.userDataDir @@ -2027,6 +2029,17 @@ private constructor( */ fun locale(locale: JsonField) = apply { this.locale = locale } + fun port(port: Double) = port(JsonField.of(port)) + + /** + * Sets [Builder.port] to an arbitrary JSON value. + * + * You should usually call [Builder.port] with a well-typed [Double] value instead. + * This method is primarily for setting the field to an undocumented or not yet + * supported value. + */ + fun port(port: JsonField) = apply { this.port = port } + fun preserveUserDataDir(preserveUserDataDir: Boolean) = preserveUserDataDir(JsonField.of(preserveUserDataDir)) @@ -2119,6 +2132,7 @@ private constructor( ignoreDefaultArgs, ignoreHttpsErrors, locale, + port, preserveUserDataDir, proxy, userDataDir, @@ -2148,6 +2162,7 @@ private constructor( ignoreDefaultArgs().ifPresent { it.validate() } ignoreHttpsErrors() locale() + port() preserveUserDataDir() proxy().ifPresent { it.validate() } userDataDir() @@ -2185,6 +2200,7 @@ private constructor( (ignoreDefaultArgs.asKnown().getOrNull()?.validity() ?: 0) + (if (ignoreHttpsErrors.asKnown().isPresent) 1 else 0) + (if (locale.asKnown().isPresent) 1 else 0) + + (if (port.asKnown().isPresent) 1 else 0) + (if (preserveUserDataDir.asKnown().isPresent) 1 else 0) + (proxy.asKnown().getOrNull()?.validity() ?: 0) + (if (userDataDir.asKnown().isPresent) 1 else 0) + @@ -2868,6 +2884,7 @@ private constructor( ignoreDefaultArgs == other.ignoreDefaultArgs && ignoreHttpsErrors == other.ignoreHttpsErrors && locale == other.locale && + port == other.port && preserveUserDataDir == other.preserveUserDataDir && proxy == other.proxy && userDataDir == other.userDataDir && @@ -2891,6 +2908,7 @@ private constructor( ignoreDefaultArgs, ignoreHttpsErrors, locale, + port, preserveUserDataDir, proxy, userDataDir, @@ -2902,7 +2920,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, preserveUserDataDir=$preserveUserDataDir, proxy=$proxy, userDataDir=$userDataDir, viewport=$viewport, additionalProperties=$additionalProperties}" + "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}" } /** Browser type to use */ @@ -7300,7 +7318,6 @@ private constructor( } return other is SessionStartParams && - xSentAt == other.xSentAt && xStreamResponse == other.xStreamResponse && body == other.body && additionalHeaders == other.additionalHeaders && @@ -7308,8 +7325,8 @@ private constructor( } override fun hashCode(): Int = - Objects.hash(xSentAt, xStreamResponse, body, additionalHeaders, additionalQueryParams) + Objects.hash(xStreamResponse, body, additionalHeaders, additionalQueryParams) override fun toString() = - "SessionStartParams{xSentAt=$xSentAt, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" + "SessionStartParams{xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" } 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 a123e6a..443104b 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 @@ -486,7 +486,15 @@ private constructor( */ fun error(): Optional = error.getOptional("error") - /** Operation result (present when status is 'finished') */ + /** + * Operation result (present when status is 'finished') + * + * This arbitrary value can be deserialized into a custom type using the `convert` + * method: + * ```java + * MyClass myObject = streamEventSystemDataOutput.result().convert(MyClass.class); + * ``` + */ @JsonProperty("result") @ExcludeMissing fun _result(): JsonValue = result /** diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsync.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsync.kt index 5c9171e..f9e65e6 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsync.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsync.kt @@ -19,6 +19,8 @@ import com.browserbase.api.models.sessions.SessionNavigateParams import com.browserbase.api.models.sessions.SessionNavigateResponse import com.browserbase.api.models.sessions.SessionObserveParams import com.browserbase.api.models.sessions.SessionObserveResponse +import com.browserbase.api.models.sessions.SessionReplayParams +import com.browserbase.api.models.sessions.SessionReplayResponse import com.browserbase.api.models.sessions.SessionStartParams import com.browserbase.api.models.sessions.SessionStartResponse import com.browserbase.api.models.sessions.StreamEvent @@ -337,6 +339,41 @@ interface SessionServiceAsync { ): AsyncStreamResponse = observeStreaming(id, SessionObserveParams.none(), requestOptions) + /** Retrieves replay metrics for a session. */ + fun replay(id: String): CompletableFuture = + replay(id, SessionReplayParams.none()) + + /** @see replay */ + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture = + replay(params.toBuilder().id(id).build(), requestOptions) + + /** @see replay */ + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + ): CompletableFuture = replay(id, params, RequestOptions.none()) + + /** @see replay */ + fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture + + /** @see replay */ + fun replay(params: SessionReplayParams): CompletableFuture = + replay(params, RequestOptions.none()) + + /** @see replay */ + fun replay( + id: String, + requestOptions: RequestOptions, + ): CompletableFuture = + replay(id, SessionReplayParams.none(), requestOptions) + /** * Creates a new browser session with the specified configuration. Returns a session ID used for * all subsequent operations. @@ -739,6 +776,47 @@ interface SessionServiceAsync { ): CompletableFuture>> = observeStreaming(id, SessionObserveParams.none(), requestOptions) + /** + * Returns a raw HTTP response for `get /v1/sessions/{id}/replay`, but is otherwise the same + * as [SessionServiceAsync.replay]. + */ + fun replay(id: String): CompletableFuture> = + replay(id, SessionReplayParams.none()) + + /** @see replay */ + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> = + replay(params.toBuilder().id(id).build(), requestOptions) + + /** @see replay */ + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + ): CompletableFuture> = + replay(id, params, RequestOptions.none()) + + /** @see replay */ + fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> + + /** @see replay */ + fun replay( + params: SessionReplayParams + ): CompletableFuture> = + replay(params, RequestOptions.none()) + + /** @see replay */ + fun replay( + id: String, + requestOptions: RequestOptions, + ): CompletableFuture> = + replay(id, SessionReplayParams.none(), requestOptions) + /** * Returns a raw HTTP response for `post /v1/sessions/start`, but is otherwise the same as * [SessionServiceAsync.start]. diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsyncImpl.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsyncImpl.kt index 6b354a0..7652220 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsyncImpl.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsyncImpl.kt @@ -35,6 +35,8 @@ import com.browserbase.api.models.sessions.SessionNavigateParams import com.browserbase.api.models.sessions.SessionNavigateResponse import com.browserbase.api.models.sessions.SessionObserveParams import com.browserbase.api.models.sessions.SessionObserveResponse +import com.browserbase.api.models.sessions.SessionReplayParams +import com.browserbase.api.models.sessions.SessionReplayResponse import com.browserbase.api.models.sessions.SessionStartParams import com.browserbase.api.models.sessions.SessionStartResponse import com.browserbase.api.models.sessions.StreamEvent @@ -136,6 +138,13 @@ class SessionServiceAsyncImpl internal constructor(private val clientOptions: Cl .thenApply { it.parse() } .toAsync(clientOptions.streamHandlerExecutor) + override fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions, + ): CompletableFuture = + // get /v1/sessions/{id}/replay + withRawResponse().replay(params, requestOptions).thenApply { it.parse() } + override fun start( params: SessionStartParams, requestOptions: RequestOptions, @@ -250,7 +259,7 @@ class SessionServiceAsyncImpl internal constructor(private val clientOptions: Cl .method(HttpMethod.POST) .baseUrl(clientOptions.baseUrl()) .addPathSegments("v1", "sessions", params._pathParam(0), "end") - .body(json(clientOptions.jsonMapper, params._body())) + .apply { params._body().ifPresent { body(json(clientOptions.jsonMapper, it)) } } .build() .prepareAsync(clientOptions, params) val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) @@ -540,6 +549,39 @@ class SessionServiceAsyncImpl internal constructor(private val clientOptions: Cl } } + private val replayHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions, + ): CompletableFuture> { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "replay") + .build() + .prepareAsync(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + return request + .thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) } + .thenApply { response -> + errorHandler.handle(response).parseable { + response + .use { replayHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + } + private val startHandler: Handler = jsonHandler(clientOptions.jsonMapper) diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionService.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionService.kt index 0d56e93..1c44d08 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionService.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionService.kt @@ -18,6 +18,8 @@ import com.browserbase.api.models.sessions.SessionNavigateParams import com.browserbase.api.models.sessions.SessionNavigateResponse import com.browserbase.api.models.sessions.SessionObserveParams import com.browserbase.api.models.sessions.SessionObserveResponse +import com.browserbase.api.models.sessions.SessionReplayParams +import com.browserbase.api.models.sessions.SessionReplayResponse import com.browserbase.api.models.sessions.SessionStartParams import com.browserbase.api.models.sessions.SessionStartResponse import com.browserbase.api.models.sessions.StreamEvent @@ -324,6 +326,36 @@ interface SessionService { fun observeStreaming(id: String, requestOptions: RequestOptions): StreamResponse = observeStreaming(id, SessionObserveParams.none(), requestOptions) + /** Retrieves replay metrics for a session. */ + fun replay(id: String): SessionReplayResponse = replay(id, SessionReplayParams.none()) + + /** @see replay */ + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionReplayResponse = replay(params.toBuilder().id(id).build(), requestOptions) + + /** @see replay */ + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + ): SessionReplayResponse = replay(id, params, RequestOptions.none()) + + /** @see replay */ + fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): SessionReplayResponse + + /** @see replay */ + fun replay(params: SessionReplayParams): SessionReplayResponse = + replay(params, RequestOptions.none()) + + /** @see replay */ + fun replay(id: String, requestOptions: RequestOptions): SessionReplayResponse = + replay(id, SessionReplayParams.none(), requestOptions) + /** * Creates a new browser session with the specified configuration. Returns a session ID used for * all subsequent operations. @@ -726,6 +758,50 @@ interface SessionService { ): HttpResponseFor> = observeStreaming(id, SessionObserveParams.none(), requestOptions) + /** + * Returns a raw HTTP response for `get /v1/sessions/{id}/replay`, but is otherwise the same + * as [SessionService.replay]. + */ + @MustBeClosed + fun replay(id: String): HttpResponseFor = + replay(id, SessionReplayParams.none()) + + /** @see replay */ + @MustBeClosed + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor = + replay(params.toBuilder().id(id).build(), requestOptions) + + /** @see replay */ + @MustBeClosed + fun replay( + id: String, + params: SessionReplayParams = SessionReplayParams.none(), + ): HttpResponseFor = replay(id, params, RequestOptions.none()) + + /** @see replay */ + @MustBeClosed + fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): HttpResponseFor + + /** @see replay */ + @MustBeClosed + fun replay(params: SessionReplayParams): HttpResponseFor = + replay(params, RequestOptions.none()) + + /** @see replay */ + @MustBeClosed + fun replay( + id: String, + requestOptions: RequestOptions, + ): HttpResponseFor = + replay(id, SessionReplayParams.none(), requestOptions) + /** * Returns a raw HTTP response for `post /v1/sessions/start`, but is otherwise the same as * [SessionService.start]. diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionServiceImpl.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionServiceImpl.kt index f064761..35a7856 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionServiceImpl.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionServiceImpl.kt @@ -33,6 +33,8 @@ import com.browserbase.api.models.sessions.SessionNavigateParams import com.browserbase.api.models.sessions.SessionNavigateResponse import com.browserbase.api.models.sessions.SessionObserveParams import com.browserbase.api.models.sessions.SessionObserveResponse +import com.browserbase.api.models.sessions.SessionReplayParams +import com.browserbase.api.models.sessions.SessionReplayResponse import com.browserbase.api.models.sessions.SessionStartParams import com.browserbase.api.models.sessions.SessionStartResponse import com.browserbase.api.models.sessions.StreamEvent @@ -115,6 +117,13 @@ class SessionServiceImpl internal constructor(private val clientOptions: ClientO // post /v1/sessions/{id}/observe withRawResponse().observeStreaming(params, requestOptions).parse() + override fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions, + ): SessionReplayResponse = + // get /v1/sessions/{id}/replay + withRawResponse().replay(params, requestOptions).parse() + override fun start( params: SessionStartParams, requestOptions: RequestOptions, @@ -223,7 +232,7 @@ class SessionServiceImpl internal constructor(private val clientOptions: ClientO .method(HttpMethod.POST) .baseUrl(clientOptions.baseUrl()) .addPathSegments("v1", "sessions", params._pathParam(0), "end") - .body(json(clientOptions.jsonMapper, params._body())) + .apply { params._body().ifPresent { body(json(clientOptions.jsonMapper, it)) } } .build() .prepare(clientOptions, params) val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) @@ -489,6 +498,36 @@ class SessionServiceImpl internal constructor(private val clientOptions: ClientO } } + private val replayHandler: Handler = + jsonHandler(clientOptions.jsonMapper) + + override fun replay( + params: SessionReplayParams, + requestOptions: RequestOptions, + ): HttpResponseFor { + // We check here instead of in the params builder because this can be specified + // positionally or in the params class. + checkRequired("id", params.id().getOrNull()) + val request = + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl(clientOptions.baseUrl()) + .addPathSegments("v1", "sessions", params._pathParam(0), "replay") + .build() + .prepare(clientOptions, params) + val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) + val response = clientOptions.httpClient.execute(request, requestOptions) + return errorHandler.handle(response).parseable { + response + .use { replayHandler.handle(it) } + .also { + if (requestOptions.responseValidation!!) { + it.validate() + } + } + } + } + private val startHandler: Handler = jsonHandler(clientOptions.jsonMapper) diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ObjectMappersTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ObjectMappersTest.kt index d405053..62c4108 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ObjectMappersTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/ObjectMappersTest.kt @@ -3,12 +3,14 @@ package com.browserbase.api.core import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.exc.MismatchedInputException import com.fasterxml.jackson.module.kotlin.readValue -import java.time.LocalDateTime +import java.time.LocalDate +import java.time.LocalTime +import java.time.OffsetDateTime +import java.time.ZoneOffset import kotlin.reflect.KClass import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.catchThrowable import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource import org.junitpioneer.jupiter.cartesian.CartesianTest @@ -46,11 +48,7 @@ internal class ObjectMappersTest { val VALID_CONVERSIONS = listOf( FLOAT to DOUBLE, - FLOAT to INTEGER, - FLOAT to LONG, DOUBLE to FLOAT, - DOUBLE to INTEGER, - DOUBLE to LONG, INTEGER to FLOAT, INTEGER to DOUBLE, INTEGER to LONG, @@ -58,14 +56,6 @@ internal class ObjectMappersTest { LONG to DOUBLE, LONG to INTEGER, CLASS to MAP, - // These aren't actually valid, but coercion configs don't work for String until - // v2.14.0: https://github.com/FasterXML/jackson-databind/issues/3240 - // We currently test on v2.13.4. - BOOLEAN to STRING, - FLOAT to STRING, - DOUBLE to STRING, - INTEGER to STRING, - LONG to STRING, ) } } @@ -84,19 +74,44 @@ internal class ObjectMappersTest { } } - enum class LenientLocalDateTimeTestCase(val string: String) { - DATE("1998-04-21"), - DATE_TIME("1998-04-21T04:00:00"), - ZONED_DATE_TIME_1("1998-04-21T04:00:00+03:00"), - ZONED_DATE_TIME_2("1998-04-21T04:00:00Z"), + enum class LenientOffsetDateTimeTestCase( + val string: String, + val expectedOffsetDateTime: OffsetDateTime, + ) { + DATE( + "1998-04-21", + expectedOffsetDateTime = + OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(0, 0), ZoneOffset.UTC), + ), + DATE_TIME( + "1998-04-21T04:00:00", + expectedOffsetDateTime = + OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(4, 0), ZoneOffset.UTC), + ), + ZONED_DATE_TIME_1( + "1998-04-21T04:00:00+03:00", + expectedOffsetDateTime = + OffsetDateTime.of( + LocalDate.of(1998, 4, 21), + LocalTime.of(4, 0), + ZoneOffset.ofHours(3), + ), + ), + ZONED_DATE_TIME_2( + "1998-04-21T04:00:00Z", + expectedOffsetDateTime = + OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(4, 0), ZoneOffset.UTC), + ), } @ParameterizedTest @EnumSource - fun readLocalDateTime_lenient(testCase: LenientLocalDateTimeTestCase) { + fun readOffsetDateTime_lenient(testCase: LenientOffsetDateTimeTestCase) { val jsonMapper = jsonMapper() val json = jsonMapper.writeValueAsString(testCase.string) - assertDoesNotThrow { jsonMapper().readValue(json) } + val offsetDateTime = jsonMapper().readValue(json) + + assertThat(offsetDateTime).isEqualTo(testCase.expectedOffsetDateTime) } } 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 db47f8d..cd7f6b7 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,68 +2,39 @@ package com.browserbase.api.models.sessions -import com.browserbase.api.core.JsonValue import com.browserbase.api.core.jsonMapper -import com.browserbase.api.errors.StagehandInvalidDataException import com.fasterxml.jackson.module.kotlin.jacksonTypeRef import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows internal class ModelConfigTest { @Test - fun ofName() { - val name = "openai/gpt-5-nano" - - val modelConfig = ModelConfig.ofName(name) - - assertThat(modelConfig.name()).contains(name) - assertThat(modelConfig.modelConfigObject()).isEmpty - } - - @Test - fun ofNameRoundtrip() { - val jsonMapper = jsonMapper() - val modelConfig = ModelConfig.ofName("openai/gpt-5-nano") - - val roundtrippedModelConfig = - jsonMapper.readValue( - jsonMapper.writeValueAsString(modelConfig), - jacksonTypeRef(), - ) - - assertThat(roundtrippedModelConfig).isEqualTo(modelConfig) - } - - @Test - fun ofModelConfigObject() { - val modelConfigObject = - ModelConfig.ModelConfigObject.builder() + fun create() { + val modelConfig = + ModelConfig.builder() .modelName("openai/gpt-5-nano") .apiKey("sk-some-openai-api-key") .baseUrl("https://api.openai.com/v1") - .provider(ModelConfig.ModelConfigObject.Provider.OPENAI) + .provider(ModelConfig.Provider.OPENAI) .build() - val modelConfig = ModelConfig.ofModelConfigObject(modelConfigObject) - - assertThat(modelConfig.name()).isEmpty - assertThat(modelConfig.modelConfigObject()).contains(modelConfigObject) + 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.provider()).contains(ModelConfig.Provider.OPENAI) } @Test - fun ofModelConfigObjectRoundtrip() { + fun roundtrip() { val jsonMapper = jsonMapper() val modelConfig = - ModelConfig.ofModelConfigObject( - ModelConfig.ModelConfigObject.builder() - .modelName("openai/gpt-5-nano") - .apiKey("sk-some-openai-api-key") - .baseUrl("https://api.openai.com/v1") - .provider(ModelConfig.ModelConfigObject.Provider.OPENAI) - .build() - ) + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() val roundtrippedModelConfig = jsonMapper.readValue( @@ -73,13 +44,4 @@ internal class ModelConfigTest { assertThat(roundtrippedModelConfig).isEqualTo(modelConfig) } - - @Test - fun incompatibleJsonShapeDeserializesToUnknown() { - val value = JsonValue.from(listOf("invalid", "array")) - val modelConfig = jsonMapper().convertValue(value, jacksonTypeRef()) - - val e = assertThrows { modelConfig.validate() } - assertThat(e).hasMessageStartingWith("Unknown ") - } } 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 54acc4f..9d1e452 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 @@ -4,7 +4,6 @@ package com.browserbase.api.models.sessions import com.browserbase.api.core.JsonValue import com.browserbase.api.core.http.Headers -import java.time.OffsetDateTime import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -14,13 +13,19 @@ internal class SessionActParamsTest { fun create() { SessionActParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionActParams.XStreamResponse.TRUE) .input("Click the login button") .frameId("frameId") .options( SessionActParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .timeout(30000.0) .variables( SessionActParams.Options.Variables.builder() @@ -50,13 +55,19 @@ internal class SessionActParamsTest { val params = SessionActParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionActParams.XStreamResponse.TRUE) .input("Click the login button") .frameId("frameId") .options( SessionActParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .timeout(30000.0) .variables( SessionActParams.Options.Variables.builder() @@ -69,13 +80,7 @@ internal class SessionActParamsTest { val headers = params._headers() - assertThat(headers) - .isEqualTo( - Headers.builder() - .put("x-sent-at", "2025-01-15T10:30:00Z") - .put("x-stream-response", "true") - .build() - ) + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) } @Test @@ -96,13 +101,19 @@ internal class SessionActParamsTest { val params = SessionActParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionActParams.XStreamResponse.TRUE) .input("Click the login button") .frameId("frameId") .options( SessionActParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .timeout(30000.0) .variables( SessionActParams.Options.Variables.builder() @@ -121,7 +132,14 @@ internal class SessionActParamsTest { assertThat(body.options()) .contains( SessionActParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .timeout(30000.0) .variables( SessionActParams.Options.Variables.builder() diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionEndParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionEndParamsTest.kt index 9081b87..43a69e2 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionEndParamsTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionEndParamsTest.kt @@ -2,9 +2,7 @@ package com.browserbase.api.models.sessions -import com.browserbase.api.core.JsonValue import com.browserbase.api.core.http.Headers -import java.time.OffsetDateTime import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -14,9 +12,7 @@ internal class SessionEndParamsTest { fun create() { SessionEndParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) - ._forceBody(JsonValue.from(mapOf())) .build() } @@ -34,20 +30,12 @@ internal class SessionEndParamsTest { val params = SessionEndParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) - ._forceBody(JsonValue.from(mapOf())) .build() val headers = params._headers() - assertThat(headers) - .isEqualTo( - Headers.builder() - .put("x-sent-at", "2025-01-15T10:30:00Z") - .put("x-stream-response", "true") - .build() - ) + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) } @Test @@ -58,26 +46,4 @@ internal class SessionEndParamsTest { assertThat(headers).isEqualTo(Headers.builder().build()) } - - @Test - fun body() { - val params = - SessionEndParams.builder() - .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) - .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) - ._forceBody(JsonValue.from(mapOf())) - .build() - - val body = params._body() - - assertThat(body.__forceBody()).isEqualTo(JsonValue.from(mapOf())) - } - - @Test - fun bodyWithoutOptionalFields() { - val params = SessionEndParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() - - val body = params._body() - } } 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 2064b33..cb26cb2 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 @@ -3,7 +3,6 @@ package com.browserbase.api.models.sessions import com.browserbase.api.core.http.Headers -import java.time.OffsetDateTime import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -13,12 +12,27 @@ internal class SessionExecuteParamsTest { fun create() { SessionExecuteParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) .agentConfig( SessionExecuteParams.AgentConfig.builder() .cua(true) - .model("openai/gpt-5-nano") + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) .systemPrompt("systemPrompt") .build() @@ -33,6 +47,7 @@ internal class SessionExecuteParamsTest { .build() ) .frameId("frameId") + .shouldCache(true) .build() } @@ -61,12 +76,27 @@ internal class SessionExecuteParamsTest { val params = SessionExecuteParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) .agentConfig( SessionExecuteParams.AgentConfig.builder() .cua(true) - .model("openai/gpt-5-nano") + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) .systemPrompt("systemPrompt") .build() @@ -81,17 +111,12 @@ internal class SessionExecuteParamsTest { .build() ) .frameId("frameId") + .shouldCache(true) .build() val headers = params._headers() - assertThat(headers) - .isEqualTo( - Headers.builder() - .put("x-sent-at", "2025-01-15T10:30:00Z") - .put("x-stream-response", "true") - .build() - ) + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) } @Test @@ -119,12 +144,27 @@ internal class SessionExecuteParamsTest { val params = SessionExecuteParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) .agentConfig( SessionExecuteParams.AgentConfig.builder() .cua(true) - .model("openai/gpt-5-nano") + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) .systemPrompt("systemPrompt") .build() @@ -139,6 +179,7 @@ internal class SessionExecuteParamsTest { .build() ) .frameId("frameId") + .shouldCache(true) .build() val body = params._body() @@ -147,7 +188,23 @@ internal class SessionExecuteParamsTest { .isEqualTo( SessionExecuteParams.AgentConfig.builder() .cua(true) - .model("openai/gpt-5-nano") + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) .systemPrompt("systemPrompt") .build() @@ -163,6 +220,7 @@ internal class SessionExecuteParamsTest { .build() ) assertThat(body.frameId()).contains("frameId") + assertThat(body.shouldCache()).contains(true) } @Test diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponseTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponseTest.kt index 85a2099..e88a30e 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponseTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionExecuteResponseTest.kt @@ -49,6 +49,12 @@ internal class SessionExecuteResponseTest { ) .build() ) + .cacheEntry( + SessionExecuteResponse.Data.CacheEntry.builder() + .cacheKey("cacheKey") + .entry(JsonValue.from(mapOf())) + .build() + ) .build() ) .success(true) @@ -90,6 +96,12 @@ internal class SessionExecuteResponseTest { ) .build() ) + .cacheEntry( + SessionExecuteResponse.Data.CacheEntry.builder() + .cacheKey("cacheKey") + .entry(JsonValue.from(mapOf())) + .build() + ) .build() ) assertThat(sessionExecuteResponse.success()).isEqualTo(true) @@ -135,6 +147,12 @@ internal class SessionExecuteResponseTest { ) .build() ) + .cacheEntry( + SessionExecuteResponse.Data.CacheEntry.builder() + .cacheKey("cacheKey") + .entry(JsonValue.from(mapOf())) + .build() + ) .build() ) .success(true) 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 54b80a6..3061c8d 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 @@ -4,7 +4,6 @@ package com.browserbase.api.models.sessions import com.browserbase.api.core.JsonValue import com.browserbase.api.core.http.Headers -import java.time.OffsetDateTime import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -14,13 +13,19 @@ internal class SessionExtractParamsTest { fun create() { SessionExtractParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Extract all product names and prices from the page") .options( SessionExtractParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("#main-content") .timeout(30000.0) .build() @@ -48,13 +53,19 @@ internal class SessionExtractParamsTest { val params = SessionExtractParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Extract all product names and prices from the page") .options( SessionExtractParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("#main-content") .timeout(30000.0) .build() @@ -68,13 +79,7 @@ internal class SessionExtractParamsTest { val headers = params._headers() - assertThat(headers) - .isEqualTo( - Headers.builder() - .put("x-sent-at", "2025-01-15T10:30:00Z") - .put("x-stream-response", "true") - .build() - ) + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) } @Test @@ -92,13 +97,19 @@ internal class SessionExtractParamsTest { val params = SessionExtractParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Extract all product names and prices from the page") .options( SessionExtractParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("#main-content") .timeout(30000.0) .build() @@ -118,7 +129,14 @@ internal class SessionExtractParamsTest { assertThat(body.options()) .contains( SessionExtractParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("#main-content") .timeout(30000.0) .build() diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionNavigateParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionNavigateParamsTest.kt index 3616da2..3e48bb2 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionNavigateParamsTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionNavigateParamsTest.kt @@ -3,7 +3,6 @@ package com.browserbase.api.models.sessions import com.browserbase.api.core.http.Headers -import java.time.OffsetDateTime import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -13,7 +12,6 @@ internal class SessionNavigateParamsTest { fun create() { SessionNavigateParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionNavigateParams.XStreamResponse.TRUE) .url("https://example.com") .frameId("frameId") @@ -46,7 +44,6 @@ internal class SessionNavigateParamsTest { val params = SessionNavigateParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionNavigateParams.XStreamResponse.TRUE) .url("https://example.com") .frameId("frameId") @@ -62,13 +59,7 @@ internal class SessionNavigateParamsTest { val headers = params._headers() - assertThat(headers) - .isEqualTo( - Headers.builder() - .put("x-sent-at", "2025-01-15T10:30:00Z") - .put("x-stream-response", "true") - .build() - ) + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) } @Test @@ -89,7 +80,6 @@ internal class SessionNavigateParamsTest { val params = SessionNavigateParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionNavigateParams.XStreamResponse.TRUE) .url("https://example.com") .frameId("frameId") 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 eeb111b..f238c0e 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 @@ -3,7 +3,6 @@ package com.browserbase.api.models.sessions import com.browserbase.api.core.http.Headers -import java.time.OffsetDateTime import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -13,13 +12,19 @@ internal class SessionObserveParamsTest { fun create() { SessionObserveParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Find all clickable navigation links") .options( SessionObserveParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("nav") .timeout(30000.0) .build() @@ -42,13 +47,19 @@ internal class SessionObserveParamsTest { val params = SessionObserveParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Find all clickable navigation links") .options( SessionObserveParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("nav") .timeout(30000.0) .build() @@ -57,13 +68,7 @@ internal class SessionObserveParamsTest { val headers = params._headers() - assertThat(headers) - .isEqualTo( - Headers.builder() - .put("x-sent-at", "2025-01-15T10:30:00Z") - .put("x-stream-response", "true") - .build() - ) + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) } @Test @@ -81,13 +86,19 @@ internal class SessionObserveParamsTest { val params = SessionObserveParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Find all clickable navigation links") .options( SessionObserveParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("nav") .timeout(30000.0) .build() @@ -101,7 +112,14 @@ internal class SessionObserveParamsTest { assertThat(body.options()) .contains( SessionObserveParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("nav") .timeout(30000.0) .build() diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionReplayParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionReplayParamsTest.kt new file mode 100644 index 0000000..7bef35a --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionReplayParamsTest.kt @@ -0,0 +1,51 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.http.Headers +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionReplayParamsTest { + + @Test + fun create() { + SessionReplayParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionReplayParams.XStreamResponse.TRUE) + .build() + } + + @Test + fun pathParams() { + val params = + SessionReplayParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() + + assertThat(params._pathParam(0)).isEqualTo("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + // out-of-bound path param + assertThat(params._pathParam(1)).isEqualTo("") + } + + @Test + fun headers() { + val params = + SessionReplayParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionReplayParams.XStreamResponse.TRUE) + .build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) + } + + @Test + fun headersWithoutOptionalFields() { + val params = + SessionReplayParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() + + val headers = params._headers() + + assertThat(headers).isEqualTo(Headers.builder().build()) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionReplayResponseTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionReplayResponseTest.kt new file mode 100644 index 0000000..848c7f7 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionReplayResponseTest.kt @@ -0,0 +1,107 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.browserbase.api.models.sessions + +import com.browserbase.api.core.jsonMapper +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class SessionReplayResponseTest { + + @Test + fun create() { + val sessionReplayResponse = + SessionReplayResponse.builder() + .data( + SessionReplayResponse.Data.builder() + .addPage( + SessionReplayResponse.Data.Page.builder() + .addAction( + SessionReplayResponse.Data.Page.Action.builder() + .method("method") + .tokenUsage( + SessionReplayResponse.Data.Page.Action.TokenUsage + .builder() + .cachedInputTokens(0.0) + .inputTokens(0.0) + .outputTokens(0.0) + .reasoningTokens(0.0) + .timeMs(0.0) + .build() + ) + .build() + ) + .build() + ) + .build() + ) + .success(true) + .build() + + assertThat(sessionReplayResponse.data()) + .isEqualTo( + SessionReplayResponse.Data.builder() + .addPage( + SessionReplayResponse.Data.Page.builder() + .addAction( + SessionReplayResponse.Data.Page.Action.builder() + .method("method") + .tokenUsage( + SessionReplayResponse.Data.Page.Action.TokenUsage.builder() + .cachedInputTokens(0.0) + .inputTokens(0.0) + .outputTokens(0.0) + .reasoningTokens(0.0) + .timeMs(0.0) + .build() + ) + .build() + ) + .build() + ) + .build() + ) + assertThat(sessionReplayResponse.success()).isEqualTo(true) + } + + @Test + fun roundtrip() { + val jsonMapper = jsonMapper() + val sessionReplayResponse = + SessionReplayResponse.builder() + .data( + SessionReplayResponse.Data.builder() + .addPage( + SessionReplayResponse.Data.Page.builder() + .addAction( + SessionReplayResponse.Data.Page.Action.builder() + .method("method") + .tokenUsage( + SessionReplayResponse.Data.Page.Action.TokenUsage + .builder() + .cachedInputTokens(0.0) + .inputTokens(0.0) + .outputTokens(0.0) + .reasoningTokens(0.0) + .timeMs(0.0) + .build() + ) + .build() + ) + .build() + ) + .build() + ) + .success(true) + .build() + + val roundtrippedSessionReplayResponse = + jsonMapper.readValue( + jsonMapper.writeValueAsString(sessionReplayResponse), + jacksonTypeRef(), + ) + + assertThat(roundtrippedSessionReplayResponse).isEqualTo(sessionReplayResponse) + } +} 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 4e4e0dc..e22e941 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 @@ -4,7 +4,6 @@ package com.browserbase.api.models.sessions import com.browserbase.api.core.JsonValue import com.browserbase.api.core.http.Headers -import java.time.OffsetDateTime import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -13,7 +12,6 @@ internal class SessionStartParamsTest { @Test fun create() { SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -36,6 +34,7 @@ internal class SessionStartParamsTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -159,7 +158,6 @@ internal class SessionStartParamsTest { fun headers() { val params = SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -182,6 +180,7 @@ internal class SessionStartParamsTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -306,13 +305,7 @@ internal class SessionStartParamsTest { val headers = params._headers() - assertThat(headers) - .isEqualTo( - Headers.builder() - .put("x-sent-at", "2025-01-15T10:30:00Z") - .put("x-stream-response", "true") - .build() - ) + assertThat(headers).isEqualTo(Headers.builder().put("x-stream-response", "true").build()) } @Test @@ -328,7 +321,6 @@ internal class SessionStartParamsTest { fun body() { val params = SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -351,6 +343,7 @@ internal class SessionStartParamsTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -497,6 +490,7 @@ internal class SessionStartParamsTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() 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 6dab5ba..a28b993 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 @@ -23,7 +23,6 @@ import com.github.tomakehurst.wiremock.client.WireMock.status import com.github.tomakehurst.wiremock.client.WireMock.stubFor import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo import com.github.tomakehurst.wiremock.junit5.WireMockTest -import java.time.OffsetDateTime import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.entry import org.junit.jupiter.api.BeforeEach @@ -75,7 +74,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -98,6 +96,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -252,7 +251,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -275,6 +273,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -429,7 +428,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -452,6 +450,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -606,7 +605,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -629,6 +627,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -783,7 +782,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -806,6 +804,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -960,7 +959,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -983,6 +981,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -1137,7 +1136,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -1160,6 +1158,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -1314,7 +1313,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -1337,6 +1335,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -1491,7 +1490,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -1514,6 +1512,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -1668,7 +1667,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -1691,6 +1689,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -1845,7 +1844,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -1868,6 +1866,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -2022,7 +2021,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -2045,6 +2043,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -2199,7 +2198,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -2222,6 +2220,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -2376,7 +2375,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -2399,6 +2397,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -2553,7 +2552,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -2576,6 +2574,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -2730,7 +2729,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -2753,6 +2751,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -2905,7 +2904,6 @@ internal class ErrorHandlingTest { assertThrows { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -2928,6 +2926,7 @@ internal class ErrorHandlingTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() 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 33f0206..61376c2 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 @@ -5,6 +5,7 @@ package com.browserbase.api.services import com.browserbase.api.client.StagehandClient import com.browserbase.api.client.okhttp.StagehandOkHttpClient import com.browserbase.api.core.JsonValue +import com.browserbase.api.models.sessions.ModelConfig import com.browserbase.api.models.sessions.SessionActParams import com.browserbase.api.models.sessions.SessionStartParams import com.github.tomakehurst.wiremock.client.WireMock.anyUrl @@ -17,7 +18,6 @@ import com.github.tomakehurst.wiremock.client.WireMock.stubFor import com.github.tomakehurst.wiremock.client.WireMock.verify import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo import com.github.tomakehurst.wiremock.junit5.WireMockTest -import java.time.OffsetDateTime import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test @@ -48,7 +48,6 @@ internal class ServiceParamsTest { sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -71,6 +70,7 @@ internal class ServiceParamsTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() @@ -214,13 +214,19 @@ internal class ServiceParamsTest { sessionService.act( SessionActParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionActParams.XStreamResponse.TRUE) .input("Click the login button") .frameId("frameId") .options( SessionActParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .timeout(30000.0) .variables( SessionActParams.Options.Variables.builder() 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 d457ad4..889335e 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 @@ -5,14 +5,15 @@ package com.browserbase.api.services.async import com.browserbase.api.TestServerExtension import com.browserbase.api.client.okhttp.StagehandOkHttpClientAsync import com.browserbase.api.core.JsonValue +import com.browserbase.api.models.sessions.ModelConfig import com.browserbase.api.models.sessions.SessionActParams import com.browserbase.api.models.sessions.SessionEndParams import com.browserbase.api.models.sessions.SessionExecuteParams import com.browserbase.api.models.sessions.SessionExtractParams import com.browserbase.api.models.sessions.SessionNavigateParams import com.browserbase.api.models.sessions.SessionObserveParams +import com.browserbase.api.models.sessions.SessionReplayParams import com.browserbase.api.models.sessions.SessionStartParams -import java.time.OffsetDateTime import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -36,13 +37,19 @@ internal class SessionServiceAsyncTest { sessionServiceAsync.act( SessionActParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionActParams.XStreamResponse.TRUE) .input("Click the login button") .frameId("frameId") .options( SessionActParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .timeout(30000.0) .variables( SessionActParams.Options.Variables.builder() @@ -74,13 +81,19 @@ internal class SessionServiceAsyncTest { sessionServiceAsync.actStreaming( SessionActParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionActParams.XStreamResponse.TRUE) .input("Click the login button") .frameId("frameId") .options( SessionActParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .timeout(30000.0) .variables( SessionActParams.Options.Variables.builder() @@ -113,9 +126,7 @@ internal class SessionServiceAsyncTest { sessionServiceAsync.end( SessionEndParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) - ._forceBody(JsonValue.from(mapOf())) .build() ) @@ -139,12 +150,27 @@ internal class SessionServiceAsyncTest { sessionServiceAsync.execute( SessionExecuteParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) .agentConfig( SessionExecuteParams.AgentConfig.builder() .cua(true) - .model("openai/gpt-5-nano") + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) .systemPrompt("systemPrompt") .build() @@ -159,6 +185,7 @@ internal class SessionServiceAsyncTest { .build() ) .frameId("frameId") + .shouldCache(true) .build() ) @@ -182,12 +209,27 @@ internal class SessionServiceAsyncTest { sessionServiceAsync.executeStreaming( SessionExecuteParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) .agentConfig( SessionExecuteParams.AgentConfig.builder() .cua(true) - .model("openai/gpt-5-nano") + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) .systemPrompt("systemPrompt") .build() @@ -202,6 +244,7 @@ internal class SessionServiceAsyncTest { .build() ) .frameId("frameId") + .shouldCache(true) .build() ) @@ -226,13 +269,19 @@ internal class SessionServiceAsyncTest { sessionServiceAsync.extract( SessionExtractParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Extract all product names and prices from the page") .options( SessionExtractParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("#main-content") .timeout(30000.0) .build() @@ -265,13 +314,19 @@ internal class SessionServiceAsyncTest { sessionServiceAsync.extractStreaming( SessionExtractParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Extract all product names and prices from the page") .options( SessionExtractParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("#main-content") .timeout(30000.0) .build() @@ -305,7 +360,6 @@ internal class SessionServiceAsyncTest { sessionServiceAsync.navigate( SessionNavigateParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionNavigateParams.XStreamResponse.TRUE) .url("https://example.com") .frameId("frameId") @@ -340,13 +394,19 @@ internal class SessionServiceAsyncTest { sessionServiceAsync.observe( SessionObserveParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Find all clickable navigation links") .options( SessionObserveParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("nav") .timeout(30000.0) .build() @@ -374,13 +434,19 @@ internal class SessionServiceAsyncTest { sessionServiceAsync.observeStreaming( SessionObserveParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Find all clickable navigation links") .options( SessionObserveParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("nav") .timeout(30000.0) .build() @@ -393,6 +459,30 @@ internal class SessionServiceAsyncTest { onCompleteFuture.get() } + @Disabled("Prism tests are disabled") + @Test + fun replay() { + val client = + StagehandOkHttpClientAsync.builder() + .baseUrl(TestServerExtension.BASE_URL) + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionServiceAsync = client.sessions() + + val responseFuture = + sessionServiceAsync.replay( + SessionReplayParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionReplayParams.XStreamResponse.TRUE) + .build() + ) + + val response = responseFuture.get() + response.validate() + } + @Disabled("Prism tests are disabled") @Test fun start() { @@ -408,7 +498,6 @@ internal class SessionServiceAsyncTest { val responseFuture = sessionServiceAsync.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -431,6 +520,7 @@ internal class SessionServiceAsyncTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() 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 b7d0e6b..d9c3ea3 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 @@ -5,14 +5,15 @@ package com.browserbase.api.services.blocking import com.browserbase.api.TestServerExtension import com.browserbase.api.client.okhttp.StagehandOkHttpClient import com.browserbase.api.core.JsonValue +import com.browserbase.api.models.sessions.ModelConfig import com.browserbase.api.models.sessions.SessionActParams import com.browserbase.api.models.sessions.SessionEndParams import com.browserbase.api.models.sessions.SessionExecuteParams import com.browserbase.api.models.sessions.SessionExtractParams import com.browserbase.api.models.sessions.SessionNavigateParams import com.browserbase.api.models.sessions.SessionObserveParams +import com.browserbase.api.models.sessions.SessionReplayParams import com.browserbase.api.models.sessions.SessionStartParams -import java.time.OffsetDateTime import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -36,13 +37,19 @@ internal class SessionServiceTest { sessionService.act( SessionActParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionActParams.XStreamResponse.TRUE) .input("Click the login button") .frameId("frameId") .options( SessionActParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .timeout(30000.0) .variables( SessionActParams.Options.Variables.builder() @@ -73,13 +80,19 @@ internal class SessionServiceTest { sessionService.actStreaming( SessionActParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionActParams.XStreamResponse.TRUE) .input("Click the login button") .frameId("frameId") .options( SessionActParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .timeout(30000.0) .variables( SessionActParams.Options.Variables.builder() @@ -112,9 +125,7 @@ internal class SessionServiceTest { sessionService.end( SessionEndParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) - ._forceBody(JsonValue.from(mapOf())) .build() ) @@ -137,12 +148,27 @@ internal class SessionServiceTest { sessionService.execute( SessionExecuteParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) .agentConfig( SessionExecuteParams.AgentConfig.builder() .cua(true) - .model("openai/gpt-5-nano") + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) .systemPrompt("systemPrompt") .build() @@ -157,6 +183,7 @@ internal class SessionServiceTest { .build() ) .frameId("frameId") + .shouldCache(true) .build() ) @@ -179,12 +206,27 @@ internal class SessionServiceTest { sessionService.executeStreaming( SessionExecuteParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExecuteParams.XStreamResponse.TRUE) .agentConfig( SessionExecuteParams.AgentConfig.builder() .cua(true) - .model("openai/gpt-5-nano") + .executionModel( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) + .mode(SessionExecuteParams.AgentConfig.Mode.CUA) + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .provider(SessionExecuteParams.AgentConfig.Provider.OPENAI) .systemPrompt("systemPrompt") .build() @@ -199,6 +241,7 @@ internal class SessionServiceTest { .build() ) .frameId("frameId") + .shouldCache(true) .build() ) @@ -223,13 +266,19 @@ internal class SessionServiceTest { sessionService.extract( SessionExtractParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Extract all product names and prices from the page") .options( SessionExtractParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("#main-content") .timeout(30000.0) .build() @@ -261,13 +310,19 @@ internal class SessionServiceTest { sessionService.extractStreaming( SessionExtractParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionExtractParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Extract all product names and prices from the page") .options( SessionExtractParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("#main-content") .timeout(30000.0) .build() @@ -301,7 +356,6 @@ internal class SessionServiceTest { sessionService.navigate( SessionNavigateParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionNavigateParams.XStreamResponse.TRUE) .url("https://example.com") .frameId("frameId") @@ -335,13 +389,19 @@ internal class SessionServiceTest { sessionService.observe( SessionObserveParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Find all clickable navigation links") .options( SessionObserveParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("nav") .timeout(30000.0) .build() @@ -368,13 +428,19 @@ internal class SessionServiceTest { sessionService.observeStreaming( SessionObserveParams.builder() .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionObserveParams.XStreamResponse.TRUE) .frameId("frameId") .instruction("Find all clickable navigation links") .options( SessionObserveParams.Options.builder() - .model("openai/gpt-5-nano") + .model( + ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey("sk-some-openai-api-key") + .baseUrl("https://api.openai.com/v1") + .provider(ModelConfig.Provider.OPENAI) + .build() + ) .selector("nav") .timeout(30000.0) .build() @@ -387,6 +453,29 @@ internal class SessionServiceTest { } } + @Disabled("Prism tests are disabled") + @Test + fun replay() { + val client = + StagehandOkHttpClient.builder() + .baseUrl(TestServerExtension.BASE_URL) + .browserbaseApiKey("My Browserbase API Key") + .browserbaseProjectId("My Browserbase Project ID") + .modelApiKey("My Model API Key") + .build() + val sessionService = client.sessions() + + val response = + sessionService.replay( + SessionReplayParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xStreamResponse(SessionReplayParams.XStreamResponse.TRUE) + .build() + ) + + response.validate() + } + @Disabled("Prism tests are disabled") @Test fun start() { @@ -402,7 +491,6 @@ internal class SessionServiceTest { val response = sessionService.start( SessionStartParams.builder() - .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionStartParams.XStreamResponse.TRUE) .modelName("openai/gpt-4o") .actTimeoutMs(0.0) @@ -425,6 +513,7 @@ internal class SessionServiceTest { .ignoreDefaultArgs(true) .ignoreHttpsErrors(true) .locale("locale") + .port(0.0) .preserveUserDataDir(true) .proxy( SessionStartParams.Browser.LaunchOptions.Proxy.builder() diff --git a/stagehand-java-example/src/main/java/com/stagehand/api/example/Main.java b/stagehand-java-example/src/main/java/com/stagehand/api/example/Main.java index e2c7a9f..61f9923 100644 --- a/stagehand-java-example/src/main/java/com/stagehand/api/example/Main.java +++ b/stagehand-java-example/src/main/java/com/stagehand/api/example/Main.java @@ -5,7 +5,6 @@ import com.browserbase.api.core.JsonValue; import com.browserbase.api.core.RequestOptions; import com.browserbase.api.models.sessions.*; - import java.time.Duration; import java.util.List; import java.util.Map; @@ -35,34 +34,32 @@ public static void main(String[] args) { StagehandClient client = StagehandOkHttpClient.fromEnv(); // Start a new browser session - SessionStartResponse startResponse = client.sessions().start( - SessionStartParams.builder() - .modelName("openai/gpt-5-nano") - .build() - ); + SessionStartResponse startResponse = client.sessions() + .start(SessionStartParams.builder() + .modelName("openai/gpt-5-nano") + .build()); String sessionId = startResponse.data().sessionId(); System.out.println("Session started: " + sessionId); try { // Navigate to Hacker News - client.sessions().navigate( - SessionNavigateParams.builder() - .id(sessionId) - .url("https://news.ycombinator.com") - .build() - ); + client.sessions() + .navigate(SessionNavigateParams.builder() + .id(sessionId) + .url("https://news.ycombinator.com") + .build()); System.out.println("Navigated to Hacker News"); // Observe to find possible actions - looking for the comments link - SessionObserveResponse observeResponse = client.sessions().observe( - SessionObserveParams.builder() - .id(sessionId) - .instruction("find the link to view comments for the top post") - .build() - ); - - List results = observeResponse.data().result(); + SessionObserveResponse observeResponse = client.sessions() + .observe(SessionObserveParams.builder() + .id(sessionId) + .instruction("find the link to view comments for the top post") + .build()); + + List results = + observeResponse.data().result(); System.out.println("Found " + results.size() + " possible actions"); if (results.isEmpty()) { @@ -76,36 +73,34 @@ public static void main(String[] args) { System.out.println("Acting on: " + action.description()); // Pass the structured action to Act - SessionActResponse actResponse = client.sessions().act( - SessionActParams.builder() - .id(sessionId) - .input(action) - .build() - ); + SessionActResponse actResponse = client.sessions() + .act(SessionActParams.builder().id(sessionId).input(action).build()); System.out.println("Act completed: " + actResponse.data().result().message()); // Extract data from the page // We're now on the comments page, so extract the top comment text - SessionExtractResponse extractResponse = client.sessions().extract( - SessionExtractParams.builder() - .id(sessionId) - .instruction("extract the text of the top comment on this page") - .schema(SessionExtractParams.Schema.builder() - .putAdditionalProperty("type", JsonValue.from("object")) - .putAdditionalProperty("properties", JsonValue.from(Map.of( - "commentText", Map.of( - "type", "string", - "description", "The text content of the top comment" - ), - "author", Map.of( - "type", "string", - "description", "The username of the comment author" - ) - ))) - .putAdditionalProperty("required", JsonValue.from(List.of("commentText"))) - .build()) - .build() - ); + SessionExtractResponse extractResponse = client.sessions() + .extract(SessionExtractParams.builder() + .id(sessionId) + .instruction("extract the text of the top comment on this page") + .schema(SessionExtractParams.Schema.builder() + .putAdditionalProperty("type", JsonValue.from("object")) + .putAdditionalProperty( + "properties", + JsonValue.from(Map.of( + "commentText", + Map.of( + "type", "string", + "description", + "The text content of the top comment"), + "author", + Map.of( + "type", "string", + "description", + "The username of the comment author")))) + .putAdditionalProperty("required", JsonValue.from(List.of("commentText"))) + .build()) + .build()); // Get the extracted result JsonValue extractedResult = extractResponse.data()._result(); @@ -125,42 +120,43 @@ public static void main(String[] args) { // Use the Agent to find the author's profile // Execute runs an autonomous agent that can navigate and interact with pages // Use a longer timeout (5 minutes) since agent execution can take a while - SessionExecuteResponse executeResponse = client.sessions().execute( - SessionExecuteParams.builder() - .id(sessionId) - .executeOptions(SessionExecuteParams.ExecuteOptions.builder() - .instruction(String.format( - "Find any personal website, GitHub, LinkedIn, or other best profile URL for the Hacker News user '%s'. " + - "Click on their username to go to their profile page and look for any links they have shared. " + - "Use Google Search with their username or other details from their profile if you dont find any direct links.", - author - )) - .maxSteps(15.0) - .build()) - .agentConfig(SessionExecuteParams.AgentConfig.builder() - .model(ModelConfig.ofModelConfigObject( - ModelConfig.ModelConfigObject.builder() - .modelName("openai/gpt-5-nano") - .apiKey(System.getenv("MODEL_API_KEY")) - .build() - )) - .cua(false) - .build()) - .build(), - RequestOptions.builder().timeout(Duration.ofMinutes(5)).build() - ); - - System.out.println("Agent completed: " + executeResponse.data().result().message()); - System.out.println("Agent success: " + executeResponse.data().result().success()); - System.out.println("Agent actions taken: " + executeResponse.data().result().actions().size()); + SessionExecuteResponse executeResponse = client.sessions() + .execute( + SessionExecuteParams.builder() + .id(sessionId) + .executeOptions(SessionExecuteParams.ExecuteOptions.builder() + .instruction(String.format( + "Find any personal website, GitHub, LinkedIn, or other best" + + " profile URL for the Hacker News user '%s'. Click on their" + + " username to go to their profile page and look for any" + + " links they have shared. Use Google Search with their" + + " username or other details from their profile if you dont" + + " find any direct links.", + author)) + .maxSteps(15.0) + .build()) + .agentConfig(SessionExecuteParams.AgentConfig.builder() + .model(ModelConfig.builder() + .modelName("openai/gpt-5-nano") + .apiKey(System.getenv("MODEL_API_KEY")) + .build()) + .cua(false) + .build()) + .build(), + RequestOptions.builder() + .timeout(Duration.ofMinutes(5)) + .build()); + + System.out.println( + "Agent completed: " + executeResponse.data().result().message()); + System.out.println( + "Agent success: " + executeResponse.data().result().success()); + System.out.println("Agent actions taken: " + + executeResponse.data().result().actions().size()); } finally { // End the session to clean up resources - client.sessions().end( - SessionEndParams.builder() - .id(sessionId) - .build() - ); + client.sessions().end(SessionEndParams.builder().id(sessionId).build()); System.out.println("Session ended"); } } diff --git a/stagehand-java-proguard-test/build.gradle.kts b/stagehand-java-proguard-test/build.gradle.kts index 9641bb2..1f6dfc6 100644 --- a/stagehand-java-proguard-test/build.gradle.kts +++ b/stagehand-java-proguard-test/build.gradle.kts @@ -19,7 +19,7 @@ dependencies { testImplementation(kotlin("test")) testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") testImplementation("org.assertj:assertj-core:3.25.3") - testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4") + testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0") } tasks.shadowJar { diff --git a/stagehand-java-proguard-test/src/test/kotlin/com/browserbase/api/proguard/ProGuardCompatibilityTest.kt b/stagehand-java-proguard-test/src/test/kotlin/com/browserbase/api/proguard/ProGuardCompatibilityTest.kt index f3e08fa..1099052 100644 --- a/stagehand-java-proguard-test/src/test/kotlin/com/browserbase/api/proguard/ProGuardCompatibilityTest.kt +++ b/stagehand-java-proguard-test/src/test/kotlin/com/browserbase/api/proguard/ProGuardCompatibilityTest.kt @@ -5,7 +5,6 @@ package com.browserbase.api.proguard import com.browserbase.api.client.okhttp.StagehandOkHttpClient import com.browserbase.api.core.jsonMapper import com.browserbase.api.models.sessions.Action -import com.browserbase.api.models.sessions.ModelConfig import com.fasterxml.jackson.module.kotlin.jacksonTypeRef import kotlin.reflect.full.memberFunctions import kotlin.reflect.jvm.javaMethod @@ -73,18 +72,4 @@ internal class ProGuardCompatibilityTest { assertThat(roundtrippedAction).isEqualTo(action) } - - @Test - fun modelConfigRoundtrip() { - val jsonMapper = jsonMapper() - val modelConfig = ModelConfig.ofName("openai/gpt-5-nano") - - val roundtrippedModelConfig = - jsonMapper.readValue( - jsonMapper.writeValueAsString(modelConfig), - jacksonTypeRef(), - ) - - assertThat(roundtrippedModelConfig).isEqualTo(modelConfig) - } }