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
+
+
+
+[](https://central.sonatype.com/artifact/com.browserbase.api/stagehand-java/0.7.0)
+[](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