diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b5c77a38..05a6cd25 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,38 +1,33 @@
name: CI
on:
push:
- branches:
- - main
- pull_request:
- branches:
- - main
- - next
+ branches-ignore:
+ - 'generated'
+ - 'codegen/**'
+ - 'integrated/**'
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
jobs:
lint:
+ timeout-minutes: 10
name: lint
- runs-on: ubuntu-latest
-
+ runs-on: ${{ github.repository == 'stainless-sdks/openlayer-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4
- - name: Validate Gradle wrapper
- uses: gradle/actions/wrapper-validation@v3
-
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
8
- 17
+ 21
cache: gradle
- name: Set up Gradle
- uses: gradle/gradle-build-action@v2
+ uses: gradle/actions/setup-gradle@v4
- name: Run lints
run: ./scripts/lint
-
-
diff --git a/.github/workflows/create-releases.yml b/.github/workflows/create-releases.yml
deleted file mode 100644
index 58107af0..00000000
--- a/.github/workflows/create-releases.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-name: Create releases
-on:
- schedule:
- - cron: '0 5 * * *' # every day at 5am UTC
- push:
- branches:
- - main
-
-jobs:
- release:
- name: release
- if: github.ref == 'refs/heads/main' && github.repository == 'openlayer-ai/openlayer-java'
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
-
- - uses: stainless-api/trigger-release-please@v1
- id: release
- with:
- repo: ${{ github.event.repository.full_name }}
- stainless-api-key: ${{ secrets.STAINLESS_API_KEY }}
-
- - name: Set up Java
- if: ${{ steps.release.outputs.releases_created }}
- uses: actions/setup-java@v3
- with:
- distribution: temurin
- java-version: |
- 8
- 17
- cache: gradle
-
- - name: Set up Gradle
- if: ${{ steps.release.outputs.releases_created }}
- uses: gradle/gradle-build-action@v2
-
- - name: Publish to Sonatype
- if: ${{ steps.release.outputs.releases_created }}
- run: |
- ./gradlew --parallel --no-daemon publish
- env:
- SONATYPE_USERNAME: ${{ secrets.OPENLAYER_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
- SONATYPE_PASSWORD: ${{ secrets.OPENLAYER_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
- GPG_SIGNING_KEY_ID: ${{ secrets.OPENLAYER_SONATYPE_GPG_SIGNING_KEY_ID || secrets.GPG_SIGNING_KEY_ID }}
- GPG_SIGNING_KEY: ${{ secrets.OPENLAYER_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
- GPG_SIGNING_PASSWORD: ${{ secrets.OPENLAYER_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }}
diff --git a/.github/workflows/handle-release-pr-title-edit.yml b/.github/workflows/handle-release-pr-title-edit.yml
deleted file mode 100644
index ac2a6802..00000000
--- a/.github/workflows/handle-release-pr-title-edit.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-name: Handle release PR title edits
-on:
- pull_request:
- types:
- - edited
- - unlabeled
-
-jobs:
- update_pr_content:
- name: Update pull request content
- if: |
- ((github.event.action == 'edited' && github.event.changes.title.from != github.event.pull_request.title) ||
- (github.event.action == 'unlabeled' && github.event.label.name == 'autorelease: custom version')) &&
- startsWith(github.event.pull_request.head.ref, 'release-please--') &&
- github.event.pull_request.state == 'open' &&
- github.event.sender.login != 'stainless-bot' &&
- github.event.sender.login != 'stainless-app' &&
- github.repository == 'openlayer-ai/openlayer-java'
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: stainless-api/trigger-release-please@v1
- with:
- repo: ${{ github.event.repository.full_name }}
- stainless-api-key: ${{ secrets.STAINLESS_API_KEY }}
diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml
index de1b147f..1953ebeb 100644
--- a/.github/workflows/publish-sonatype.yml
+++ b/.github/workflows/publish-sonatype.yml
@@ -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 publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD"
+ ./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD" --no-configuration-cache
env:
SONATYPE_USERNAME: ${{ secrets.OPENLAYER_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.OPENLAYER_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
diff --git a/.gitignore b/.gitignore
index 39c31e3e..4e81838d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
.prism.log
.gradle
.idea
+.kotlin
build
codegen.log
kls_database.db
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index ee49ac2d..fd0ccba9 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.1.0-alpha.11"
+ ".": "0.1.0-alpha.12"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index dd473053..2b09528b 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1,3 @@
-configured_endpoints: 14
+configured_endpoints: 18
+openapi_spec_hash: 20f058101a252f7500803d66aff58eb3
+config_hash: 30422a4611d93ca69e4f1aff60b9ddb5
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab2c9931..3beb0138 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,36 @@
# Changelog
+## 0.1.0-alpha.12 (2025-05-22)
+
+Full Changelog: [v0.1.0-alpha.11...v0.1.0-alpha.12](https://github.com/openlayer-ai/openlayer-java/compare/v0.1.0-alpha.11...v0.1.0-alpha.12)
+
+### ⚠ BREAKING CHANGES
+
+* **client:** extract auto pagination to shared classes
+* **client:** **Migration:** - If you were referencing the `AutoPager` class on a specific `*Page` or `*PageAsync` type, then you should instead reference the shared `AutoPager` and `AutoPagerAsync` types, under the `core` package
+ - `AutoPagerAsync` now has different usage. You can call `.subscribe(...)` on the returned object instead to get called back each page item. You can also call `onCompleteFuture()` to get a future that completes when all items have been processed. Finally, you can call `.close()` on the returned object to stop auto-paginating early
+ - If you were referencing `getNextPage` or `getNextPageParams`:
+ - Swap to `nextPage()` and `nextPageParams()`
+ - Note that these both now return non-optional types (use `hasNextPage()` before calling these, since they will throw if it's impossible to get another page)
+
+### Features
+
+* **client:** allow providing some params positionally ([134e615](https://github.com/openlayer-ai/openlayer-java/commit/134e61553124243d105b41cfc2cfbd886dce4e63))
+* **client:** extract auto pagination to shared classes ([d2fa723](https://github.com/openlayer-ai/openlayer-java/commit/d2fa72386ce83cb182c3d710ef25cd9b4b84bd99))
+
+
+### Chores
+
+* **docs:** grammar improvements ([d1cacea](https://github.com/openlayer-ai/openlayer-java/commit/d1cacea72d2b3f2de5d20028b2c5227ad2138534))
+* **internal:** codegen related update ([ce52e15](https://github.com/openlayer-ai/openlayer-java/commit/ce52e159a41280088862b8be8b5dd41194051ed9))
+* sync repo ([390e75d](https://github.com/openlayer-ai/openlayer-java/commit/390e75d8719363b5395eed54c1ce3c9d8d0412fb))
+
+
+### Documentation
+
+* add Spring AI sample application ([4cc1fb8](https://github.com/openlayer-ai/openlayer-java/commit/4cc1fb87584f47679b4e1be12be9f1d1e796481b))
+* remove or fix invalid readme examples ([9372b90](https://github.com/openlayer-ai/openlayer-java/commit/9372b9032b7e05eb6055a5e71acc6570422146a5))
+
## 0.1.0-alpha.11 (2024-12-20)
Full Changelog: [v0.1.0-alpha.10...v0.1.0-alpha.11](https://github.com/openlayer-ai/openlayer-java/compare/v0.1.0-alpha.10...v0.1.0-alpha.11)
diff --git a/LICENSE b/LICENSE
index 82530825..ac864c56 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2024 Openlayer
+ Copyright 2025 Openlayer
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index ed0596e7..ae1190fd 100644
--- a/README.md
+++ b/README.md
@@ -2,203 +2,321 @@
-[](https://central.sonatype.com/artifact/com.openlayer.api/openlayer-java/0.1.0-alpha.11)
+[](https://central.sonatype.com/artifact/com.openlayer.api/openlayer-java/0.1.0-alpha.12)
+[](https://javadoc.io/doc/com.openlayer.api/openlayer-java/0.1.0-alpha.12)
-The Openlayer Java SDK provides convenient access to the Openlayer REST API from applications written in Java. It includes helper classes with helpful types and documentation for every request and response property.
+The Openlayer Java SDK provides convenient access to the [Openlayer REST API](https://openlayer.com/docs/api-reference/rest/overview) from applications written in Java.
-The Openlayer Java SDK is similar to the Openlayer 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/).
-It is generated with [Stainless](https://www.stainlessapi.com/).
-
-## Documentation
-
-The REST API documentation can be found on [openlayer.com](https://openlayer.com/docs/api-reference/rest/overview).
-
----
+
-## Getting started
+The REST API documentation can be found on [openlayer.com](https://openlayer.com/docs/api-reference/rest/overview). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.openlayer.api/openlayer-java/0.1.0-alpha.12).
-### Install dependencies
+
-#### Gradle
+## Installation
+### Gradle
+
```kotlin
-implementation("com.openlayer.api:openlayer-java:0.1.0-alpha.11")
+implementation("com.openlayer.api:openlayer-java:0.1.0-alpha.12")
```
-#### Maven
+### Maven
```xml
- com.openlayer.api
- openlayer-java
- 0.1.0-alpha.11
+ com.openlayer.api
+ openlayer-java
+ 0.1.0-alpha.12
```
-### Configure the client
+## Requirements
-Use `OpenlayerOkHttpClient.builder()` to configure the client.
+This library requires Java 8 or later.
-Alternately, set the environment with `OPENLAYER_API_KEY`, and use `OpenlayerOkHttpClient.fromEnv()` to read from the environment.
+## Usage
```java
import com.openlayer.api.client.OpenlayerClient;
import com.openlayer.api.client.okhttp.OpenlayerOkHttpClient;
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
+// Configures using the `OPENLAYER_API_KEY` and `OPENLAYER_BASE_URL` environment variables
OpenlayerClient client = OpenlayerOkHttpClient.fromEnv();
-// Note: you can also call fromEnv() from the client builder, for example if you need to set additional properties
+DataStreamParams params = DataStreamParams.builder()
+ .inferencePipelineId("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
+ .config(DataStreamParams.Config.LlmData.builder()
+ .addInputVariableName("user_query")
+ .outputColumnName("output")
+ .numOfTokenColumnName("tokens")
+ .costColumnName("cost")
+ .timestampColumnName("timestamp")
+ .build())
+ .addRow(DataStreamParams.Row.builder()
+ .putAdditionalProperty("user_query", JsonValue.from("what is the meaning of life?"))
+ .putAdditionalProperty("output", JsonValue.from("42"))
+ .putAdditionalProperty("tokens", JsonValue.from(7))
+ .putAdditionalProperty("cost", JsonValue.from(0.02))
+ .putAdditionalProperty("timestamp", JsonValue.from(1610000000))
+ .build())
+ .build();
+DataStreamResponse response = client.inferencePipelines().data().stream(params);
+```
+
+## Client configuration
+
+Configure the client using environment variables:
+
+```java
+import com.openlayer.api.client.OpenlayerClient;
+import com.openlayer.api.client.okhttp.OpenlayerOkHttpClient;
+
+// Configures using the `OPENLAYER_API_KEY` and `OPENLAYER_BASE_URL` environment variables
+OpenlayerClient client = OpenlayerOkHttpClient.fromEnv();
+```
+
+Or manually:
+
+```java
+import com.openlayer.api.client.OpenlayerClient;
+import com.openlayer.api.client.okhttp.OpenlayerOkHttpClient;
+
OpenlayerClient client = OpenlayerOkHttpClient.builder()
+ .apiKey("My API Key")
+ .build();
+```
+
+Or using a combination of the two approaches:
+
+```java
+import com.openlayer.api.client.OpenlayerClient;
+import com.openlayer.api.client.okhttp.OpenlayerOkHttpClient;
+
+OpenlayerClient client = OpenlayerOkHttpClient.builder()
+ // Configures using the `OPENLAYER_API_KEY` and `OPENLAYER_BASE_URL` environment variables
.fromEnv()
- // ... set properties on the builder
+ .apiKey("My API Key")
.build();
```
-| Property | Environment variable | Required | Default value |
-| -------- | -------------------- | -------- | ------------- |
-| apiKey | `OPENLAYER_API_KEY` | false | — |
+See this table for the available options:
+
+| Setter | Environment variable | Required | Default value |
+| --------- | -------------------- | -------- | -------------------------------- |
+| `apiKey` | `OPENLAYER_API_KEY` | false | - |
+| `baseUrl` | `OPENLAYER_BASE_URL` | true | `"https://api.openlayer.com/v1"` |
+
+> [!TIP]
+> Don't create more than one client in the same application. Each client has a connection pool and
+> thread pools, which are more efficient to share between requests.
+
+## Requests and responses
+
+To send a request to the Openlayer API, build an instance of some `Params` class and pass it to the corresponding client method. When the response is received, it will be deserialized into an instance of a Java class.
-Read the documentation for more configuration options.
+For example, `client.inferencePipelines().data().stream(...)` should be called with an instance of `DataStreamParams`, and it will return an instance of `DataStreamResponse`.
----
+## Immutability
-### Example: creating a resource
+Each class in the SDK has an associated [builder](https://blogs.oracle.com/javamagazine/post/exploring-joshua-blochs-builder-design-pattern-in-java) or factory method for constructing it.
-To create a new inference pipeline data, first use the `InferencePipelineDataStreamParams` builder to specify attributes, then pass that to the `stream` method of the `data` service.
+Each class is [immutable](https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html) once constructed. If the class has an associated builder, then it has a `toBuilder()` method, which can be used to convert it back to a builder for making a modified copy.
+
+Because each class is immutable, builder modification will _never_ affect already built class instances.
+
+## Asynchronous execution
+
+The default client is synchronous. To switch to asynchronous execution, call the `async()` method:
```java
+import com.openlayer.api.client.OpenlayerClient;
+import com.openlayer.api.client.okhttp.OpenlayerOkHttpClient;
import com.openlayer.api.core.JsonValue;
-import com.openlayer.api.models.InferencePipelineDataStreamParams;
-import com.openlayer.api.models.InferencePipelineDataStreamResponse;
-import java.util.List;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
+import java.util.concurrent.CompletableFuture;
-InferencePipelineDataStreamParams params = InferencePipelineDataStreamParams.builder()
+// Configures using the `OPENLAYER_API_KEY` and `OPENLAYER_BASE_URL` environment variables
+OpenlayerClient client = OpenlayerOkHttpClient.fromEnv();
+
+DataStreamParams params = DataStreamParams.builder()
.inferencePipelineId("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
- .config(InferencePipelineDataStreamParams.Config.ofLlmData(InferencePipelineDataStreamParams.Config.LlmData.builder()
- .inputVariableNames(List.of("user_query"))
+ .config(DataStreamParams.Config.LlmData.builder()
+ .addInputVariableName("user_query")
.outputColumnName("output")
.numOfTokenColumnName("tokens")
.costColumnName("cost")
.timestampColumnName("timestamp")
- .build()))
- .rows(List.of(InferencePipelineDataStreamParams.Row.builder()
+ .build())
+ .addRow(DataStreamParams.Row.builder()
.putAdditionalProperty("user_query", JsonValue.from("what is the meaning of life?"))
.putAdditionalProperty("output", JsonValue.from("42"))
.putAdditionalProperty("tokens", JsonValue.from(7))
.putAdditionalProperty("cost", JsonValue.from(0.02))
.putAdditionalProperty("timestamp", JsonValue.from(1610000000))
- .build()))
+ .build())
.build();
-InferencePipelineDataStreamResponse response = client.inferencePipelines().data().stream(params);
+CompletableFuture response = client.async().inferencePipelines().data().stream(params);
```
----
+Or create an asynchronous client from the beginning:
-## Requests
+```java
+import com.openlayer.api.client.OpenlayerClientAsync;
+import com.openlayer.api.client.okhttp.OpenlayerOkHttpClientAsync;
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
+import java.util.concurrent.CompletableFuture;
-### Parameters and bodies
+// Configures using the `OPENLAYER_API_KEY` and `OPENLAYER_BASE_URL` environment variables
+OpenlayerClientAsync client = OpenlayerOkHttpClientAsync.fromEnv();
-To make a request to the Openlayer API, you generally build an instance of the appropriate `Params` class.
+DataStreamParams params = DataStreamParams.builder()
+ .inferencePipelineId("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
+ .config(DataStreamParams.Config.LlmData.builder()
+ .addInputVariableName("user_query")
+ .outputColumnName("output")
+ .numOfTokenColumnName("tokens")
+ .costColumnName("cost")
+ .timestampColumnName("timestamp")
+ .build())
+ .addRow(DataStreamParams.Row.builder()
+ .putAdditionalProperty("user_query", JsonValue.from("what is the meaning of life?"))
+ .putAdditionalProperty("output", JsonValue.from("42"))
+ .putAdditionalProperty("tokens", JsonValue.from(7))
+ .putAdditionalProperty("cost", JsonValue.from(0.02))
+ .putAdditionalProperty("timestamp", JsonValue.from(1610000000))
+ .build())
+ .build();
+CompletableFuture response = client.inferencePipelines().data().stream(params);
+```
+
+The asynchronous client supports the same options as the synchronous one, except most methods return `CompletableFuture`s.
+
+## Raw responses
-In [Example: creating a resource](#example-creating-a-resource) above, we used the `InferencePipelineDataStreamParams.builder()` to pass to the `stream` method of the `data` service.
+The SDK defines methods that deserialize responses into instances of Java classes. However, these methods don't provide access to the response headers, status code, or the raw response body.
-Sometimes, the API may support other properties that are not yet supported in the Java SDK types. In that case, you can attach them using the `putAdditionalProperty` method.
+To access this data, prefix any HTTP method call on a client or service with `withRawResponse()`:
```java
import com.openlayer.api.core.JsonValue;
-import com.openlayer.api.models.InferencePipelineDataStreamParams;
+import com.openlayer.api.core.http.Headers;
+import com.openlayer.api.core.http.HttpResponseFor;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
-InferencePipelineDataStreamParams params = InferencePipelineDataStreamParams.builder()
- // ... normal properties
- .putAdditionalProperty("secret_param", JsonValue.from("4242"))
+DataStreamParams params = DataStreamParams.builder()
+ .inferencePipelineId("182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e")
+ .config(DataStreamParams.Config.LlmData.builder()
+ .addInputVariableName("user_query")
+ .outputColumnName("output")
+ .numOfTokenColumnName("tokens")
+ .costColumnName("cost")
+ .timestampColumnName("timestamp")
+ .build())
+ .addRow(DataStreamParams.Row.builder()
+ .putAdditionalProperty("user_query", JsonValue.from("what is the meaning of life?"))
+ .putAdditionalProperty("output", JsonValue.from("42"))
+ .putAdditionalProperty("tokens", JsonValue.from(7))
+ .putAdditionalProperty("cost", JsonValue.from(0.02))
+ .putAdditionalProperty("timestamp", JsonValue.from(1610000000))
+ .build())
.build();
-```
+HttpResponseFor response = client.inferencePipelines().data().withRawResponse().stream(params);
-## Responses
-
-### Response validation
+int statusCode = response.statusCode();
+Headers headers = response.headers();
+```
-When receiving a response, the Openlayer Java SDK will deserialize it into instances of the typed model classes. In rare cases, the API may return a response property that doesn't match the expected Java type. If you directly access the mistaken property, the SDK will throw an unchecked `OpenlayerInvalidDataException` at runtime. If you would prefer to check in advance that that response is completely well-typed, call `.validate()` on the returned model.
+You can still deserialize the response into an instance of a Java class if needed:
```java
-import com.openlayer.api.models.InferencePipelineDataStreamResponse;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
-InferencePipelineDataStreamResponse response = client.inferencePipelines().data().stream().validate();
+DataStreamResponse parsedResponse = response.parse();
```
-### Response properties as JSON
+## Error handling
-In rare cases, you may want to access the underlying JSON value for a response property rather than using the typed version provided by this SDK. Each model property has a corresponding JSON version, with an underscore before the method name, which returns a `JsonField` value.
+The SDK throws custom unchecked exception types:
-```java
-import com.openlayer.api.core.JsonField;
-import java.util.Optional;
+- [`OpenlayerServiceException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/OpenlayerServiceException.kt): Base class for HTTP errors. See this table for which exception subclass is thrown for each HTTP status code:
-JsonField field = responseObj._field();
+ | Status | Exception |
+ | ------ | -------------------------------------------------------------------------------------------------------------------------------- |
+ | 400 | [`BadRequestException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/BadRequestException.kt) |
+ | 401 | [`UnauthorizedException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/UnauthorizedException.kt) |
+ | 403 | [`PermissionDeniedException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/PermissionDeniedException.kt) |
+ | 404 | [`NotFoundException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/NotFoundException.kt) |
+ | 422 | [`UnprocessableEntityException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/UnprocessableEntityException.kt) |
+ | 429 | [`RateLimitException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/RateLimitException.kt) |
+ | 5xx | [`InternalServerException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/InternalServerException.kt) |
+ | others | [`UnexpectedStatusCodeException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/UnexpectedStatusCodeException.kt) |
-if (field.isMissing()) {
- // Value was not specified in the JSON response
-} else if (field.isNull()) {
- // Value was provided as a literal null
-} else {
- // See if value was provided as a string
- Optional jsonString = field.asString();
+- [`OpenlayerIoException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/OpenlayerIoException.kt): I/O networking errors.
- // If the value given by the API did not match the shape that the SDK expects
- // you can deserialise into a custom type
- MyClass myObj = responseObj._field().asUnknown().orElseThrow().convert(MyClass.class);
-}
-```
+- [`OpenlayerInvalidDataException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/OpenlayerInvalidDataException.kt): Failure to interpret successfully parsed data. For example, when accessing a property that's supposed to be required, but the API unexpectedly omitted it from the response.
-### Additional model properties
+- [`OpenlayerException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/OpenlayerException.kt): Base class for all exceptions. Most errors will result in one of the previously mentioned ones, but completely generic errors may be thrown using the base class.
-Sometimes, the server response may include additional properties that are not yet available in this library's types. You can access them using the model's `_additionalProperties` method:
+## Logging
-```java
-import com.openlayer.api.core.JsonValue;
+The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
-JsonValue secret = projectCreateResponse._additionalProperties().get("secret_field");
-```
+Enable logging by setting the `OPENLAYER_LOG` environment variable to `info`:
----
+```sh
+$ export OPENLAYER_LOG=info
+```
----
+Or to `debug` for more verbose logging:
-## Error handling
+```sh
+$ export OPENLAYER_LOG=debug
+```
-This library throws exceptions in a single hierarchy for easy handling:
+## Jackson
-- **`OpenlayerException`** - Base exception for all exceptions
+The SDK depends on [Jackson](https://github.com/FasterXML/jackson) for JSON serialization/deserialization. It is compatible with version 2.13.4 or higher, but depends on version 2.18.2 by default.
-- **`OpenlayerServiceException`** - HTTP errors with a well-formed response body we were able to parse. The exception message and the `.debuggingRequestId()` will be set by the server.
+The SDK throws an exception if it detects an incompatible Jackson version at runtime (e.g. if the default version was overridden in your Maven or Gradle config).
- | 400 | BadRequestException |
- | ------ | ----------------------------- |
- | 401 | AuthenticationException |
- | 403 | PermissionDeniedException |
- | 404 | NotFoundException |
- | 422 | UnprocessableEntityException |
- | 429 | RateLimitException |
- | 5xx | InternalServerException |
- | others | UnexpectedStatusCodeException |
+If the SDK threw an exception, but you're _certain_ the version is compatible, then disable the version check using the `checkJacksonVersionCompatibility` on [`OpenlayerOkHttpClient`](openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt) or [`OpenlayerOkHttpClientAsync`](openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClientAsync.kt).
-- **`OpenlayerIoException`** - I/O networking errors
-- **`OpenlayerInvalidDataException`** - any other exceptions on the client side, e.g.:
- - We failed to serialize the request body
- - We failed to parse the response body (has access to response code and body)
+> [!CAUTION]
+> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
## Network options
### Retries
-Requests that experience certain errors are automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors will all be retried by default. You can provide a `maxRetries` on the client builder to configure this:
+The SDK automatically retries 2 times by default, with a short exponential backoff.
+
+Only the following error types are retried:
+
+- Connection errors (for example, due to a network connectivity problem)
+- 408 Request Timeout
+- 409 Conflict
+- 429 Rate Limit
+- 5xx Internal
+
+The API may also explicitly instruct the SDK to retry or not retry a response.
+
+To set a custom number of retries, configure the client using the `maxRetries` method:
```java
import com.openlayer.api.client.OpenlayerClient;
@@ -212,7 +330,19 @@ OpenlayerClient client = OpenlayerOkHttpClient.builder()
### Timeouts
-Requests time out after 1 minute by default. You can configure this on the client builder:
+Requests time out after 1 minute by default.
+
+To set a custom timeout, configure the method call using the `timeout` method:
+
+```java
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
+
+DataStreamResponse response = client.inferencePipelines().data().stream(
+ params, RequestOptions.builder().timeout(Duration.ofSeconds(30)).build()
+);
+```
+
+Or configure the default for all method calls at the client level:
```java
import com.openlayer.api.client.OpenlayerClient;
@@ -227,7 +357,7 @@ OpenlayerClient client = OpenlayerOkHttpClient.builder()
### Proxies
-Requests can be routed through a proxy. You can configure this on the client builder:
+To route requests through a proxy, configure the client using the `proxy` method:
```java
import com.openlayer.api.client.OpenlayerClient;
@@ -237,56 +367,294 @@ import java.net.Proxy;
OpenlayerClient client = OpenlayerOkHttpClient.builder()
.fromEnv()
- .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("example.com", 8080)))
+ .proxy(new Proxy(
+ Proxy.Type.HTTP, new InetSocketAddress(
+ "https://example.com", 8080
+ )
+ ))
.build();
```
-## Making custom/undocumented requests
+### Custom HTTP client
-This library is typed for convenient access to the documented API. If you need to access undocumented params or response properties, the library can still be used.
+The SDK consists of three artifacts:
-### Undocumented request params
+- `openlayer-java-core`
+ - Contains core SDK logic
+ - Does not depend on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`OpenlayerClient`](openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClient.kt), [`OpenlayerClientAsync`](openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsync.kt), [`OpenlayerClientImpl`](openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientImpl.kt), and [`OpenlayerClientAsyncImpl`](openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsyncImpl.kt), all of which can work with any HTTP client
+- `openlayer-java-client-okhttp`
+ - Depends on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`OpenlayerOkHttpClient`](openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt) and [`OpenlayerOkHttpClientAsync`](openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClientAsync.kt), which provide a way to construct [`OpenlayerClientImpl`](openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientImpl.kt) and [`OpenlayerClientAsyncImpl`](openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsyncImpl.kt), respectively, using OkHttp
+- `openlayer-java`
+ - Depends on and exposes the APIs of both `openlayer-java-core` and `openlayer-java-client-okhttp`
+ - Does not have its own logic
-To make requests using undocumented parameters, you can provide or override parameters on the params object while building it.
+This structure allows replacing the SDK's default HTTP client without pulling in unnecessary dependencies.
-```kotlin
-FooCreateParams address = FooCreateParams.builder()
- .id("my_id")
- .putAdditionalProperty("secret_prop", JsonValue.from("hello"))
+#### Customized [`OkHttpClient`](https://square.github.io/okhttp/3.x/okhttp/okhttp3/OkHttpClient.html)
+
+> [!TIP]
+> Try the available [network options](#network-options) before replacing the default client.
+
+To use a customized `OkHttpClient`:
+
+1. Replace your [`openlayer-java` dependency](#installation) with `openlayer-java-core`
+2. Copy `openlayer-java-client-okhttp`'s [`OkHttpClient`](openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OkHttpClient.kt) class into your code and customize it
+3. Construct [`OpenlayerClientImpl`](openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientImpl.kt) or [`OpenlayerClientAsyncImpl`](openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsyncImpl.kt), similarly to [`OpenlayerOkHttpClient`](openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt) or [`OpenlayerOkHttpClientAsync`](openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClientAsync.kt), using your customized client
+
+### Completely custom HTTP client
+
+To use a completely custom HTTP client:
+
+1. Replace your [`openlayer-java` dependency](#installation) with `openlayer-java-core`
+2. Write a class that implements the [`HttpClient`](openlayer-java-core/src/main/kotlin/com/openlayer/api/core/http/HttpClient.kt) interface
+3. Construct [`OpenlayerClientImpl`](openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientImpl.kt) or [`OpenlayerClientAsyncImpl`](openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsyncImpl.kt), similarly to [`OpenlayerOkHttpClient`](openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt) or [`OpenlayerOkHttpClientAsync`](openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClientAsync.kt), using your new client class
+
+## Undocumented API functionality
+
+The SDK is typed for convenient usage of the documented API. However, it also supports working with undocumented or not yet supported parts of the API.
+
+### Parameters
+
+To set undocumented parameters, call the `putAdditionalHeader`, `putAdditionalQueryParam`, or `putAdditionalBodyProperty` methods on any `Params` class:
+
+```java
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+
+DataStreamParams params = DataStreamParams.builder()
+ .putAdditionalHeader("Secret-Header", "42")
+ .putAdditionalQueryParam("secret_query_param", "42")
+ .putAdditionalBodyProperty("secretProperty", JsonValue.from("42"))
.build();
```
-### Undocumented response properties
+These can be accessed on the built object later using the `_additionalHeaders()`, `_additionalQueryParams()`, and `_additionalBodyProperties()` methods.
-To access undocumented response properties, you can use `res._additionalProperties()` on a response object to get a map of untyped fields of type `Map`. You can then access fields like `._additionalProperties().get("secret_prop").asString()` or use other helpers defined on the `JsonValue` class to extract it to a desired type.
+To set undocumented parameters on _nested_ headers, query params, or body classes, call the `putAdditionalProperty` method on the nested class:
-## Logging
+```java
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.projects.ProjectCreateParams;
-We use the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
+ProjectCreateParams params = ProjectCreateParams.builder()
+ .links(ProjectCreateParams.Links.builder()
+ .putAdditionalProperty("secretProperty", JsonValue.from("42"))
+ .build())
+ .build();
+```
-You can enable logging by setting the environment variable `OPENLAYER_LOG` to `info`.
+These properties can be accessed on the nested built object later using the `_additionalProperties()` method.
-```sh
-$ export OPENLAYER_LOG=info
+To set a documented parameter or property to an undocumented or not yet supported _value_, pass a [`JsonValue`](openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt) object to its setter:
+
+```java
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+
+DataStreamParams params = DataStreamParams.builder()
+ .config(JsonValue.from(42))
+ .addRow(DataStreamParams.Row.builder()
+ .putAdditionalProperty("user_query", JsonValue.from("what is the meaning of life?"))
+ .putAdditionalProperty("output", JsonValue.from("42"))
+ .putAdditionalProperty("tokens", JsonValue.from(7))
+ .putAdditionalProperty("cost", JsonValue.from(0.02))
+ .putAdditionalProperty("timestamp", JsonValue.from(1610000000))
+ .build())
+ .build();
```
-Or to `debug` for more verbose logging.
+The most straightforward way to create a [`JsonValue`](openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt) is using its `from(...)` method:
-```sh
-$ export OPENLAYER_LOG=debug
+```java
+import com.openlayer.api.core.JsonValue;
+import java.util.List;
+import java.util.Map;
+
+// Create primitive JSON values
+JsonValue nullValue = JsonValue.from(null);
+JsonValue booleanValue = JsonValue.from(true);
+JsonValue numberValue = JsonValue.from(42);
+JsonValue stringValue = JsonValue.from("Hello World!");
+
+// Create a JSON array value equivalent to `["Hello", "World"]`
+JsonValue arrayValue = JsonValue.from(List.of(
+ "Hello", "World"
+));
+
+// Create a JSON object value equivalent to `{ "a": 1, "b": 2 }`
+JsonValue objectValue = JsonValue.from(Map.of(
+ "a", 1,
+ "b", 2
+));
+
+// Create an arbitrarily nested JSON equivalent to:
+// {
+// "a": [1, 2],
+// "b": [3, 4]
+// }
+JsonValue complexValue = JsonValue.from(Map.of(
+ "a", List.of(
+ 1, 2
+ ),
+ "b", List.of(
+ 3, 4
+ )
+));
```
+Normally a `Builder` class's `build` method will throw [`IllegalStateException`](https://docs.oracle.com/javase/8/docs/api/java/lang/IllegalStateException.html) if any required parameter or property is unset.
+
+To forcibly omit a required parameter or property, pass [`JsonMissing`](openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt):
+
+```java
+import com.openlayer.api.core.JsonMissing;
+import com.openlayer.api.core.JsonValue;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+
+DataStreamParams params = DataStreamParams.builder()
+ .config(DataStreamParams.Config.LlmData.builder()
+ .outputColumnName("output")
+ .build())
+ .addRow(DataStreamParams.Row.builder()
+ .putAdditionalProperty("user_query", JsonValue.from("bar"))
+ .putAdditionalProperty("output", JsonValue.from("bar"))
+ .putAdditionalProperty("tokens", JsonValue.from("bar"))
+ .putAdditionalProperty("cost", JsonValue.from("bar"))
+ .putAdditionalProperty("timestamp", JsonValue.from("bar"))
+ .build())
+ .inferencePipelineId(JsonMissing.of())
+ .build();
+```
+
+### Response properties
+
+To access undocumented response properties, call the `_additionalProperties()` method:
+
+```java
+import com.openlayer.api.core.JsonValue;
+import java.util.Map;
+
+Map additionalProperties = client.inferencePipelines().data().stream(params)._additionalProperties();
+JsonValue secretPropertyValue = additionalProperties.get("secretProperty");
+
+String result = secretPropertyValue.accept(new JsonValue.Visitor<>() {
+ @Override
+ public String visitNull() {
+ return "It's null!";
+ }
+
+ @Override
+ public String visitBoolean(boolean value) {
+ return "It's a boolean!";
+ }
+
+ @Override
+ public String visitNumber(Number value) {
+ return "It's a number!";
+ }
+
+ // Other methods include `visitMissing`, `visitString`, `visitArray`, and `visitObject`
+ // The default implementation of each unimplemented method delegates to `visitDefault`, which throws by default, but can also be overridden
+});
+```
+
+To access a property's raw JSON value, which may be undocumented, call its `_` prefixed method:
+
+```java
+import com.openlayer.api.core.JsonField;
+import com.openlayer.api.models.inferencepipelines.data.DataStreamParams;
+import java.util.Optional;
+
+JsonField config = client.inferencePipelines().data().stream(params)._config();
+
+if (config.isMissing()) {
+ // The property is absent from the JSON response
+} else if (config.isNull()) {
+ // The property was set to literal null
+} else {
+ // Check if value was provided as a string
+ // Other methods include `asNumber()`, `asBoolean()`, etc.
+ Optional jsonString = config.asString();
+
+ // Try to deserialize into a custom type
+ MyClass myObject = config.asUnknown().orElseThrow().convert(MyClass.class);
+}
+```
+
+### Response validation
+
+In rare cases, the API may return a response that doesn't match the expected type. For example, the SDK may expect a property to contain a `String`, but the API could return something else.
+
+By default, the SDK will not throw an exception in this case. It will throw [`OpenlayerInvalidDataException`](openlayer-java-core/src/main/kotlin/com/openlayer/api/errors/OpenlayerInvalidDataException.kt) only if you directly access the property.
+
+If you would prefer to check that the response is completely well-typed upfront, then either call `validate()`:
+
+```java
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
+
+DataStreamResponse response = client.inferencePipelines().data().stream(params).validate();
+```
+
+Or configure the method call to validate the response using the `responseValidation` method:
+
+```java
+import com.openlayer.api.models.inferencepipelines.data.DataStreamResponse;
+
+DataStreamResponse response = client.inferencePipelines().data().stream(
+ params, RequestOptions.builder().responseValidation(true).build()
+);
+```
+
+Or configure the default for all method calls at the client level:
+
+```java
+import com.openlayer.api.client.OpenlayerClient;
+import com.openlayer.api.client.okhttp.OpenlayerOkHttpClient;
+
+OpenlayerClient client = OpenlayerOkHttpClient.builder()
+ .fromEnv()
+ .responseValidation(true)
+ .build();
+```
+
+## FAQ
+
+### Why don't you use plain `enum` classes?
+
+Java `enum` classes are not trivially [forwards compatible](https://www.stainless.com/blog/making-java-enums-forwards-compatible). Using them in the SDK could cause runtime exceptions if the API is updated to respond with a new enum value.
+
+### Why do you represent fields using `JsonField` instead of just plain `T`?
+
+Using `JsonField` enables a few features:
+
+- Allowing usage of [undocumented API functionality](#undocumented-api-functionality)
+- Lazily [validating the API response against the expected shape](#response-validation)
+- Representing absent vs explicitly null values
+
+### Why don't you use [`data` classes](https://kotlinlang.org/docs/data-classes.html)?
+
+It is not [backwards compatible to add new fields to a data class](https://kotlinlang.org/docs/api-guidelines-backward-compatibility.html#avoid-using-data-classes-in-your-api) and we don't want to introduce a breaking change every time we add a field to a class.
+
+### Why don't you use checked exceptions?
+
+Checked exceptions are widely considered a mistake in the Java programming language. In fact, they were omitted from Kotlin for this reason.
+
+Checked exceptions:
+
+- Are verbose to handle
+- Encourage error handling at the wrong level of abstraction, where nothing can be done about the error
+- Are tedious to propagate due to the [function coloring problem](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function)
+- Don't play well with lambdas (also due to the function coloring problem)
+
## Semantic versioning
This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
-1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals)_.
+1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
2. Changes that we do not expect to impact the vast majority of users in practice.
We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
We are keen for your feedback; please open an [issue](https://www.github.com/openlayer-ai/openlayer-java/issues) with questions, bugs, or suggestions.
-
-## Requirements
-
-This library requires Java 8 or later.
diff --git a/SECURITY.md b/SECURITY.md
index 6dfa13e4..dc108d01 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,9 +2,9 @@
## Reporting Security Issues
-This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
+This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
-To report a security issue, please contact the Stainless team at security@stainlessapi.com.
+To report a security issue, please contact the Stainless team at security@stainless.com.
## Responsible Disclosure
@@ -16,11 +16,11 @@ before making any information public.
## Reporting Non-SDK Related Security Issues
If you encounter security issues that are not directly related to SDKs but pertain to the services
-or products provided by Openlayer please follow the respective company's security reporting guidelines.
+or products provided by Openlayer, please follow the respective company's security reporting guidelines.
### Openlayer Terms and Policies
-Please contact support@openlayer.com for any questions or concerns regarding security of our services.
+Please contact support@openlayer.com for any questions or concerns regarding the security of our services.
---
diff --git a/build.gradle.kts b/build.gradle.kts
index 6466dce7..e74d61c0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,10 +1,23 @@
plugins {
+ id("org.jetbrains.dokka") version "2.0.0"
+}
+repositories {
+ mavenCentral()
}
allprojects {
group = "com.openlayer.api"
- version = "0.1.0-alpha.11" // x-release-please-version
+ version = "0.1.0-alpha.12" // x-release-please-version
}
+subprojects {
+ apply(plugin = "org.jetbrains.dokka")
+}
+// Avoid race conditions between `dokkaJavadocCollector` and `dokkaJavadocJar` tasks
+tasks.named("dokkaJavadocCollector").configure {
+ subprojects.flatMap { it.tasks }
+ .filter { it.project.name != "openlayer-java" && it.name == "dokkaJavadocJar" }
+ .forEach { mustRunAfter(it) }
+}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 493cb327..778c89de 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -1,6 +1,6 @@
plugins {
`kotlin-dsl`
- kotlin("jvm") version "1.9.22"
+ kotlin("jvm") version "1.9.20"
id("com.vanniktech.maven.publish") version "0.28.0"
}
@@ -10,7 +10,7 @@ repositories {
}
dependencies {
- implementation("com.diffplug.spotless:spotless-plugin-gradle:6.25.0")
- implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23")
+ implementation("com.diffplug.spotless:spotless-plugin-gradle:7.0.2")
+ 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/openlayer.java.gradle.kts b/buildSrc/src/main/kotlin/openlayer.java.gradle.kts
index 32a150ed..dfbacb86 100644
--- a/buildSrc/src/main/kotlin/openlayer.java.gradle.kts
+++ b/buildSrc/src/main/kotlin/openlayer.java.gradle.kts
@@ -1,9 +1,5 @@
import com.diffplug.gradle.spotless.SpotlessExtension
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
-import com.vanniktech.maven.publish.JavaLibrary
-import com.vanniktech.maven.publish.JavadocJar
-import com.vanniktech.maven.publish.MavenPublishBaseExtension
-import com.vanniktech.maven.publish.SonatypeHost
plugins {
`java-library`
@@ -25,8 +21,11 @@ configure {
java {
toolchain {
- languageVersion.set(JavaLanguageVersion.of(17))
+ languageVersion.set(JavaLanguageVersion.of(21))
}
+
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
}
tasks.withType().configureEach {
@@ -43,9 +42,13 @@ tasks.named("jar") {
}
}
-tasks.named("test") {
+tasks.withType().configureEach {
useJUnitPlatform()
+ // Run tests in parallel to some degree.
+ maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
+ forkEvery = 100
+
testLogging {
exceptionFormat = TestExceptionFormat.FULL
}
diff --git a/buildSrc/src/main/kotlin/openlayer.kotlin.gradle.kts b/buildSrc/src/main/kotlin/openlayer.kotlin.gradle.kts
index b0bd08ab..47f50550 100644
--- a/buildSrc/src/main/kotlin/openlayer.kotlin.gradle.kts
+++ b/buildSrc/src/main/kotlin/openlayer.kotlin.gradle.kts
@@ -1,6 +1,6 @@
import com.diffplug.gradle.spotless.SpotlessExtension
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-import com.vanniktech.maven.publish.*
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
plugins {
id("openlayer.java")
@@ -9,7 +9,21 @@ plugins {
kotlin {
jvmToolchain {
- languageVersion.set(JavaLanguageVersion.of(17))
+ languageVersion.set(JavaLanguageVersion.of(21))
+ }
+
+ compilerOptions {
+ freeCompilerArgs = listOf(
+ "-Xjvm-default=all",
+ "-Xjdk-release=1.8",
+ // Suppress deprecation warnings because we may still reference and test deprecated members.
+ // TODO: Replace with `-Xsuppress-warning=DEPRECATION` once we use Kotlin compiler 2.1.0+.
+ "-nowarn",
+ )
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ languageVersion.set(KotlinVersion.KOTLIN_1_8)
+ apiVersion.set(KotlinVersion.KOTLIN_1_8)
+ coreLibrariesVersion = "1.8.0"
}
}
@@ -20,10 +34,7 @@ configure {
}
}
-tasks.withType().configureEach {
- kotlinOptions {
- allWarningsAsErrors = true
- freeCompilerArgs = listOf("-Xjvm-default=all", "-Xjdk-release=1.8")
- jvmTarget = "1.8"
- }
+tasks.withType().configureEach {
+ systemProperty("junit.jupiter.execution.parallel.enabled", true)
+ systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
}
diff --git a/buildSrc/src/main/kotlin/openlayer.publish.gradle.kts b/buildSrc/src/main/kotlin/openlayer.publish.gradle.kts
index d159a4fa..5f23d62a 100644
--- a/buildSrc/src/main/kotlin/openlayer.publish.gradle.kts
+++ b/buildSrc/src/main/kotlin/openlayer.publish.gradle.kts
@@ -1,10 +1,5 @@
-import org.gradle.api.publish.PublishingExtension
-import org.gradle.api.publish.maven.MavenPublication
-import org.gradle.kotlin.dsl.configure
-import org.gradle.kotlin.dsl.register
-import org.gradle.kotlin.dsl.get
-import com.vanniktech.maven.publish.JavaLibrary
import com.vanniktech.maven.publish.JavadocJar
+import com.vanniktech.maven.publish.KotlinJvm
import com.vanniktech.maven.publish.MavenPublishBaseExtension
import com.vanniktech.maven.publish.SonatypeHost
@@ -25,7 +20,13 @@ configure {
signAllPublications()
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
- this.coordinates(project.group.toString(), project.name, project.version.toString())
+ coordinates(project.group.toString(), project.name, project.version.toString())
+ configure(
+ KotlinJvm(
+ javadocJar = JavadocJar.Dokka("dokkaJavadoc"),
+ sourcesJar = true,
+ )
+ )
pom {
name.set("Openlayer API")
diff --git a/examples/spring-ai-example/.mvn/wrapper/maven-wrapper.properties b/examples/spring-ai-example/.mvn/wrapper/maven-wrapper.properties
deleted file mode 100644
index d58dfb70..00000000
--- a/examples/spring-ai-example/.mvn/wrapper/maven-wrapper.properties
+++ /dev/null
@@ -1,19 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you 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.
-wrapperVersion=3.3.2
-distributionType=only-script
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
diff --git a/examples/spring-ai-example/README.md b/examples/spring-ai-example/README.md
deleted file mode 100644
index d1e772c4..00000000
--- a/examples/spring-ai-example/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# Spring AI sample application
-
-## Prerequisites
-
-- Java 21+
-- An Openlayer API key
-- An OpenAI API key
-
-## How to run
-
-1. Set the environment variables.
- ```
- export SPRING_AI_OPENAI_APIKEY="sk-proj-xxx"
- export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.openlayer.ai/v1/otel"
- export OTEL_EXPORTER_OTLP_HEADERS=""Authorization=Bearer YOUR_OPENLAYER_API_KEY, x-bt-parent=pipeline_id:YOUR_OPENLAYER_PIPELINE_ID""
- ```
-2. Run the application with `./mvnw clean install spring-boot:run`.
-3. Call the chat endpoint with `curl localhost:8080/v1/chat`.
-
-The trace will automatically be sent to Openlayer.
diff --git a/examples/spring-ai-example/mvnw b/examples/spring-ai-example/mvnw
deleted file mode 100755
index 19529ddf..00000000
--- a/examples/spring-ai-example/mvnw
+++ /dev/null
@@ -1,259 +0,0 @@
-#!/bin/sh
-# ----------------------------------------------------------------------------
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you 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.
-# ----------------------------------------------------------------------------
-
-# ----------------------------------------------------------------------------
-# Apache Maven Wrapper startup batch script, version 3.3.2
-#
-# Optional ENV vars
-# -----------------
-# JAVA_HOME - location of a JDK home dir, required when download maven via java source
-# MVNW_REPOURL - repo url base for downloading maven distribution
-# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
-# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
-# ----------------------------------------------------------------------------
-
-set -euf
-[ "${MVNW_VERBOSE-}" != debug ] || set -x
-
-# OS specific support.
-native_path() { printf %s\\n "$1"; }
-case "$(uname)" in
-CYGWIN* | MINGW*)
- [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
- native_path() { cygpath --path --windows "$1"; }
- ;;
-esac
-
-# set JAVACMD and JAVACCMD
-set_java_home() {
- # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
- if [ -n "${JAVA_HOME-}" ]; then
- if [ -x "$JAVA_HOME/jre/sh/java" ]; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
- JAVACCMD="$JAVA_HOME/jre/sh/javac"
- else
- JAVACMD="$JAVA_HOME/bin/java"
- JAVACCMD="$JAVA_HOME/bin/javac"
-
- if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
- echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
- echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
- return 1
- fi
- fi
- else
- JAVACMD="$(
- 'set' +e
- 'unset' -f command 2>/dev/null
- 'command' -v java
- )" || :
- JAVACCMD="$(
- 'set' +e
- 'unset' -f command 2>/dev/null
- 'command' -v javac
- )" || :
-
- if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
- echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
- return 1
- fi
- fi
-}
-
-# hash string like Java String::hashCode
-hash_string() {
- str="${1:-}" h=0
- while [ -n "$str" ]; do
- char="${str%"${str#?}"}"
- h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
- str="${str#?}"
- done
- printf %x\\n $h
-}
-
-verbose() { :; }
-[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
-
-die() {
- printf %s\\n "$1" >&2
- exit 1
-}
-
-trim() {
- # MWRAPPER-139:
- # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
- # Needed for removing poorly interpreted newline sequences when running in more
- # exotic environments such as mingw bash on Windows.
- printf "%s" "${1}" | tr -d '[:space:]'
-}
-
-# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
-while IFS="=" read -r key value; do
- case "${key-}" in
- distributionUrl) distributionUrl=$(trim "${value-}") ;;
- distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
- esac
-done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
-[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
-
-case "${distributionUrl##*/}" in
-maven-mvnd-*bin.*)
- MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
- case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
- *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
- :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
- :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
- :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
- *)
- echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
- distributionPlatform=linux-amd64
- ;;
- esac
- distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
- ;;
-maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
-*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
-esac
-
-# apply MVNW_REPOURL and calculate MAVEN_HOME
-# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
-[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
-distributionUrlName="${distributionUrl##*/}"
-distributionUrlNameMain="${distributionUrlName%.*}"
-distributionUrlNameMain="${distributionUrlNameMain%-bin}"
-MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
-MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
-
-exec_maven() {
- unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
- exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
-}
-
-if [ -d "$MAVEN_HOME" ]; then
- verbose "found existing MAVEN_HOME at $MAVEN_HOME"
- exec_maven "$@"
-fi
-
-case "${distributionUrl-}" in
-*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
-*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
-esac
-
-# prepare tmp dir
-if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
- clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
- trap clean HUP INT TERM EXIT
-else
- die "cannot create temp dir"
-fi
-
-mkdir -p -- "${MAVEN_HOME%/*}"
-
-# Download and Install Apache Maven
-verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
-verbose "Downloading from: $distributionUrl"
-verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
-
-# select .zip or .tar.gz
-if ! command -v unzip >/dev/null; then
- distributionUrl="${distributionUrl%.zip}.tar.gz"
- distributionUrlName="${distributionUrl##*/}"
-fi
-
-# verbose opt
-__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
-[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
-
-# normalize http auth
-case "${MVNW_PASSWORD:+has-password}" in
-'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
-has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
-esac
-
-if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
- verbose "Found wget ... using wget"
- wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
-elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
- verbose "Found curl ... using curl"
- curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
-elif set_java_home; then
- verbose "Falling back to use Java to download"
- javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
- targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
- cat >"$javaSource" <<-END
- public class Downloader extends java.net.Authenticator
- {
- protected java.net.PasswordAuthentication getPasswordAuthentication()
- {
- return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
- }
- public static void main( String[] args ) throws Exception
- {
- setDefault( new Downloader() );
- java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
- }
- }
- END
- # For Cygwin/MinGW, switch paths to Windows format before running javac and java
- verbose " - Compiling Downloader.java ..."
- "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
- verbose " - Running Downloader.java ..."
- "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
-fi
-
-# If specified, validate the SHA-256 sum of the Maven distribution zip file
-if [ -n "${distributionSha256Sum-}" ]; then
- distributionSha256Result=false
- if [ "$MVN_CMD" = mvnd.sh ]; then
- echo "Checksum validation is not supported for maven-mvnd." >&2
- echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
- exit 1
- elif command -v sha256sum >/dev/null; then
- if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
- distributionSha256Result=true
- fi
- elif command -v shasum >/dev/null; then
- if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
- distributionSha256Result=true
- fi
- else
- echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
- echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
- exit 1
- fi
- if [ $distributionSha256Result = false ]; then
- echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
- echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
- exit 1
- fi
-fi
-
-# unzip and move
-if command -v unzip >/dev/null; then
- unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
-else
- tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
-fi
-printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
-mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
-
-clean || :
-exec_maven "$@"
diff --git a/examples/spring-ai-example/mvnw.cmd b/examples/spring-ai-example/mvnw.cmd
deleted file mode 100644
index b150b91e..00000000
--- a/examples/spring-ai-example/mvnw.cmd
+++ /dev/null
@@ -1,149 +0,0 @@
-<# : batch portion
-@REM ----------------------------------------------------------------------------
-@REM Licensed to the Apache Software Foundation (ASF) under one
-@REM or more contributor license agreements. See the NOTICE file
-@REM distributed with this work for additional information
-@REM regarding copyright ownership. The ASF licenses this file
-@REM to you under the Apache License, Version 2.0 (the
-@REM "License"); you may not use this file except in compliance
-@REM with the License. You may obtain a copy of the License at
-@REM
-@REM http://www.apache.org/licenses/LICENSE-2.0
-@REM
-@REM Unless required by applicable law or agreed to in writing,
-@REM software distributed under the License is distributed on an
-@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-@REM KIND, either express or implied. See the License for the
-@REM specific language governing permissions and limitations
-@REM under the License.
-@REM ----------------------------------------------------------------------------
-
-@REM ----------------------------------------------------------------------------
-@REM Apache Maven Wrapper startup batch script, version 3.3.2
-@REM
-@REM Optional ENV vars
-@REM MVNW_REPOURL - repo url base for downloading maven distribution
-@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
-@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
-@REM ----------------------------------------------------------------------------
-
-@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
-@SET __MVNW_CMD__=
-@SET __MVNW_ERROR__=
-@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
-@SET PSModulePath=
-@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
- IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
-)
-@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
-@SET __MVNW_PSMODULEP_SAVE=
-@SET __MVNW_ARG0_NAME__=
-@SET MVNW_USERNAME=
-@SET MVNW_PASSWORD=
-@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
-@echo Cannot start maven from wrapper >&2 && exit /b 1
-@GOTO :EOF
-: end batch / begin powershell #>
-
-$ErrorActionPreference = "Stop"
-if ($env:MVNW_VERBOSE -eq "true") {
- $VerbosePreference = "Continue"
-}
-
-# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
-$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
-if (!$distributionUrl) {
- Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
-}
-
-switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
- "maven-mvnd-*" {
- $USE_MVND = $true
- $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
- $MVN_CMD = "mvnd.cmd"
- break
- }
- default {
- $USE_MVND = $false
- $MVN_CMD = $script -replace '^mvnw','mvn'
- break
- }
-}
-
-# apply MVNW_REPOURL and calculate MAVEN_HOME
-# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
-if ($env:MVNW_REPOURL) {
- $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
- $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
-}
-$distributionUrlName = $distributionUrl -replace '^.*/',''
-$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
-$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
-if ($env:MAVEN_USER_HOME) {
- $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
-}
-$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
-$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
-
-if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
- Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
- Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
- exit $?
-}
-
-if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
- Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
-}
-
-# prepare tmp dir
-$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
-$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
-$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
-trap {
- if ($TMP_DOWNLOAD_DIR.Exists) {
- try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
- catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
- }
-}
-
-New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
-
-# Download and Install Apache Maven
-Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
-Write-Verbose "Downloading from: $distributionUrl"
-Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
-
-$webclient = New-Object System.Net.WebClient
-if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
- $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
-}
-[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
-$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
-
-# If specified, validate the SHA-256 sum of the Maven distribution zip file
-$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
-if ($distributionSha256Sum) {
- if ($USE_MVND) {
- Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
- }
- Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
- if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
- Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
- }
-}
-
-# unzip and move
-Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
-Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
-try {
- Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
-} catch {
- if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
- Write-Error "fail to move MAVEN_HOME"
- }
-} finally {
- try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
- catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
-}
-
-Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/examples/spring-ai-example/pom.xml b/examples/spring-ai-example/pom.xml
deleted file mode 100644
index 4f3a4cbb..00000000
--- a/examples/spring-ai-example/pom.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
- 4.0.0
-
- org.springframework.boot
- spring-boot-starter-parent
- 3.4.3
-
-
- com.openlayer
- spring-ai-demo
- 0.0.1-SNAPSHOT
- spring-ai-demo
- Spring AI demo project for Spring Boot
-
-
-
- 21
-
-
-
-
-
- io.opentelemetry.instrumentation
- opentelemetry-instrumentation-bom
- 2.13.2
- pom
- import
-
-
-
-
-
-
- org.springframework.boot
- spring-boot-starter
-
-
- org.springframework.ai
- spring-ai-openai-spring-boot-starter
- 1.0.0-M6
-
-
-
- org.springframework.boot
- spring-boot-starter-web
-
-
- io.opentelemetry.instrumentation
- opentelemetry-spring-boot-starter
-
-
-
- org.springframework.boot
- spring-boot-starter-actuator
-
-
-
- io.micrometer
- micrometer-tracing-bridge-otel
-
-
-
- io.opentelemetry
- opentelemetry-exporter-otlp
-
-
-
-
-
-
- org.springframework.boot
- spring-boot-maven-plugin
-
-
-
-
diff --git a/examples/spring-ai-example/src/main/java/com/openlayer/springai/ChatController.java b/examples/spring-ai-example/src/main/java/com/openlayer/springai/ChatController.java
deleted file mode 100644
index 1a0f0c73..00000000
--- a/examples/spring-ai-example/src/main/java/com/openlayer/springai/ChatController.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.openlayer.springai;
-
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/v1/chat")
-public class ChatController {
-
- private final ChatService chatService;
-
- public ChatController(ChatService chatService) {
- this.chatService = chatService;
- }
-
- @GetMapping
- public String get() {
- return chatService.testAiCall();
- }
-}
diff --git a/examples/spring-ai-example/src/main/java/com/openlayer/springai/ChatService.java b/examples/spring-ai-example/src/main/java/com/openlayer/springai/ChatService.java
deleted file mode 100644
index 209fc8eb..00000000
--- a/examples/spring-ai-example/src/main/java/com/openlayer/springai/ChatService.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.openlayer.springai;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.ai.chat.client.ChatClient;
-import org.springframework.boot.context.event.ApplicationReadyEvent;
-import org.springframework.context.event.EventListener;
-import org.springframework.stereotype.Service;
-
-@Service
-public class ChatService {
-
- static final Logger LOGGER = LoggerFactory.getLogger(ChatService.class);
-
- private final ChatClient chatClient;
-
- public ChatService(ChatClient.Builder builder) {
- chatClient = builder.build();
- }
-
- @EventListener(ApplicationReadyEvent.class)
- public String testAiCall() {
- LOGGER.info("Invoking LLM");
- String answer = chatClient.prompt("What's the meaning of life?").call().content();
- LOGGER.info("AI answered: {}", answer);
- return answer;
- }
-}
diff --git a/examples/spring-ai-example/src/main/java/com/openlayer/springai/SpringAiDemoApplication.java b/examples/spring-ai-example/src/main/java/com/openlayer/springai/SpringAiDemoApplication.java
deleted file mode 100644
index 09d0c184..00000000
--- a/examples/spring-ai-example/src/main/java/com/openlayer/springai/SpringAiDemoApplication.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.openlayer.springai;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class SpringAiDemoApplication {
- public static void main(String[] args) {
- SpringApplication.run(SpringAiDemoApplication.class, args);
- }
-}
diff --git a/examples/spring-ai-example/src/main/resources/application.yml b/examples/spring-ai-example/src/main/resources/application.yml
deleted file mode 100644
index d026980d..00000000
--- a/examples/spring-ai-example/src/main/resources/application.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-spring:
- application:
- name: java-demo
- ai:
- chat:
- observations:
- include-prompt: true # Include prompt content in tracing (disabled by default for privacy)
- include-completion: true # Include completion content in tracing (disabled by default)
-server:
- port: 8081
-management:
- tracing:
- sampling:
- probability: 1.0 # Sample 100% of requests for full tracing (adjust in production as needed)
- observations:
- annotations:
- enabled: true # Enable @Observed (if you use observation annotations in code)
diff --git a/examples/spring-ai-example/target/classes/application.yml b/examples/spring-ai-example/target/classes/application.yml
deleted file mode 100644
index d026980d..00000000
--- a/examples/spring-ai-example/target/classes/application.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-spring:
- application:
- name: java-demo
- ai:
- chat:
- observations:
- include-prompt: true # Include prompt content in tracing (disabled by default for privacy)
- include-completion: true # Include completion content in tracing (disabled by default)
-server:
- port: 8081
-management:
- tracing:
- sampling:
- probability: 1.0 # Sample 100% of requests for full tracing (adjust in production as needed)
- observations:
- annotations:
- enabled: true # Enable @Observed (if you use observation annotations in code)
diff --git a/examples/spring-ai-example/target/classes/com/openlayer/springai/ChatController.class b/examples/spring-ai-example/target/classes/com/openlayer/springai/ChatController.class
deleted file mode 100644
index c8d8d2a5..00000000
Binary files a/examples/spring-ai-example/target/classes/com/openlayer/springai/ChatController.class and /dev/null differ
diff --git a/examples/spring-ai-example/target/classes/com/openlayer/springai/ChatService.class b/examples/spring-ai-example/target/classes/com/openlayer/springai/ChatService.class
deleted file mode 100644
index b72f579a..00000000
Binary files a/examples/spring-ai-example/target/classes/com/openlayer/springai/ChatService.class and /dev/null differ
diff --git a/examples/spring-ai-example/target/classes/com/openlayer/springai/SpringAiDemoApplication.class b/examples/spring-ai-example/target/classes/com/openlayer/springai/SpringAiDemoApplication.class
deleted file mode 100644
index 4e3c6cb0..00000000
Binary files a/examples/spring-ai-example/target/classes/com/openlayer/springai/SpringAiDemoApplication.class and /dev/null differ
diff --git a/examples/spring-ai-example/target/maven-archiver/pom.properties b/examples/spring-ai-example/target/maven-archiver/pom.properties
deleted file mode 100644
index cb4875ad..00000000
--- a/examples/spring-ai-example/target/maven-archiver/pom.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-artifactId=spring-ai-demo
-groupId=com.openlayer
-version=0.0.1-SNAPSHOT
diff --git a/examples/spring-ai-example/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/examples/spring-ai-example/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
deleted file mode 100644
index ef9811f3..00000000
--- a/examples/spring-ai-example/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
+++ /dev/null
@@ -1,3 +0,0 @@
-com/openlayer/springai/ChatService.class
-com/openlayer/springai/ChatController.class
-com/openlayer/springai/SpringAiDemoApplication.class
diff --git a/examples/spring-ai-example/target/spring-ai-demo-0.0.1-SNAPSHOT.jar b/examples/spring-ai-example/target/spring-ai-demo-0.0.1-SNAPSHOT.jar
deleted file mode 100644
index 4e095efd..00000000
Binary files a/examples/spring-ai-example/target/spring-ai-demo-0.0.1-SNAPSHOT.jar and /dev/null differ
diff --git a/examples/spring-ai-example/target/spring-ai-demo-0.0.1-SNAPSHOT.jar.original b/examples/spring-ai-example/target/spring-ai-demo-0.0.1-SNAPSHOT.jar.original
deleted file mode 100644
index 39c81680..00000000
Binary files a/examples/spring-ai-example/target/spring-ai-demo-0.0.1-SNAPSHOT.jar.original and /dev/null differ
diff --git a/gradle.properties b/gradle.properties
index a3bc58f2..ff76593f 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,17 @@
org.gradle.caching=true
-org.gradle.jvmargs=-Xmx4g
+org.gradle.configuration-cache=true
org.gradle.parallel=true
-kotlin.daemon.jvmargs=-Xmx4g
+org.gradle.daemon=false
+# These options improve our compilation and test performance. They are inherited by the Kotlin daemon.
+org.gradle.jvmargs=\
+ -Xms1g \
+ -Xmx4g \
+ -XX:+UseParallelGC \
+ -XX:InitialCodeCacheSize=256m \
+ -XX:ReservedCodeCacheSize=1G \
+ -XX:MetaspaceSize=256m \
+ -XX:TieredStopAtLevel=1 \
+ -XX:GCTimeRatio=4 \
+ -XX:CICompilerCount=4 \
+ -XX:+OptimizeStringConcat \
+ -XX:+UseStringDeduplication
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e6441136..a4b76b95 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b82aa23a..cea7a793 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index 1aa94a42..f3b75f3b 100755
--- a/gradlew
+++ b/gradlew
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
diff --git a/gradlew.bat b/gradlew.bat
index 25da30db..9d21a218 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
diff --git a/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OkHttpClient.kt b/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OkHttpClient.kt
index cd4ff7dc..83f7a9cf 100644
--- a/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OkHttpClient.kt
+++ b/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OkHttpClient.kt
@@ -1,6 +1,8 @@
package com.openlayer.api.client.okhttp
import com.openlayer.api.core.RequestOptions
+import com.openlayer.api.core.Timeout
+import com.openlayer.api.core.checkRequired
import com.openlayer.api.core.http.Headers
import com.openlayer.api.core.http.HttpClient
import com.openlayer.api.core.http.HttpMethod
@@ -30,38 +32,8 @@ class OkHttpClient
private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val baseUrl: HttpUrl) :
HttpClient {
- private fun getClient(requestOptions: RequestOptions): okhttp3.OkHttpClient {
- val clientBuilder = okHttpClient.newBuilder()
-
- val logLevel =
- when (System.getenv("OPENLAYER_LOG")?.lowercase()) {
- "info" -> HttpLoggingInterceptor.Level.BASIC
- "debug" -> HttpLoggingInterceptor.Level.BODY
- else -> null
- }
- if (logLevel != null) {
- clientBuilder.addNetworkInterceptor(
- HttpLoggingInterceptor().setLevel(logLevel).apply { redactHeader("Authorization") }
- )
- }
-
- val timeout = requestOptions.timeout
- if (timeout != null) {
- clientBuilder
- .connectTimeout(timeout)
- .readTimeout(timeout)
- .writeTimeout(timeout)
- .callTimeout(if (timeout.seconds == 0L) timeout else timeout.plusSeconds(30))
- }
-
- return clientBuilder.build()
- }
-
- override fun execute(
- request: HttpRequest,
- requestOptions: RequestOptions,
- ): HttpResponse {
- val call = getClient(requestOptions).newCall(request.toRequest())
+ override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
+ val call = newCall(request, requestOptions)
return try {
call.execute().toResponse()
@@ -80,18 +52,18 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
request.body?.run { future.whenComplete { _, _ -> close() } }
- val call = getClient(requestOptions).newCall(request.toRequest())
- call.enqueue(
- object : Callback {
- override fun onResponse(call: Call, response: Response) {
- future.complete(response.toResponse())
- }
+ newCall(request, requestOptions)
+ .enqueue(
+ object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ future.complete(response.toResponse())
+ }
- override fun onFailure(call: Call, e: IOException) {
- future.completeExceptionally(OpenlayerIoException("Request failed", e))
+ override fun onFailure(call: Call, e: IOException) {
+ future.completeExceptionally(OpenlayerIoException("Request failed", e))
+ }
}
- }
- )
+ )
return future
}
@@ -102,10 +74,36 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
okHttpClient.cache?.close()
}
- private fun HttpRequest.toRequest(): Request {
+ private fun newCall(request: HttpRequest, requestOptions: RequestOptions): Call {
+ val clientBuilder = okHttpClient.newBuilder()
+
+ val logLevel =
+ when (System.getenv("OPENLAYER_LOG")?.lowercase()) {
+ "info" -> HttpLoggingInterceptor.Level.BASIC
+ "debug" -> HttpLoggingInterceptor.Level.BODY
+ else -> null
+ }
+ if (logLevel != null) {
+ clientBuilder.addNetworkInterceptor(
+ HttpLoggingInterceptor().setLevel(logLevel).apply { redactHeader("Authorization") }
+ )
+ }
+
+ requestOptions.timeout?.let {
+ clientBuilder
+ .connectTimeout(it.connect())
+ .readTimeout(it.read())
+ .writeTimeout(it.write())
+ .callTimeout(it.request())
+ }
+
+ val client = clientBuilder.build()
+ return client.newCall(request.toRequest(client))
+ }
+
+ private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient): Request {
var body: RequestBody? = body?.toRequestBody()
- // OkHttpClient always requires a request body for PUT and POST methods.
- if (body == null && (method == HttpMethod.PUT || method == HttpMethod.POST)) {
+ if (body == null && requiresBody(method)) {
body = "".toRequestBody()
}
@@ -114,9 +112,33 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
headers.values(name).forEach { builder.header(name, it) }
}
+ if (
+ !headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
+ ) {
+ builder.header(
+ "X-Stainless-Read-Timeout",
+ Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
+ if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
+ builder.header(
+ "X-Stainless-Timeout",
+ Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
+
return builder.build()
}
+ /** `OkHttpClient` always requires a request body for some methods. */
+ private fun requiresBody(method: HttpMethod): Boolean =
+ when (method) {
+ HttpMethod.POST,
+ HttpMethod.PUT,
+ HttpMethod.PATCH -> true
+ else -> false
+ }
+
private fun HttpRequest.toUrl(): String {
url?.let {
return it
@@ -170,29 +192,30 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
@JvmStatic fun builder() = Builder()
}
- class Builder {
+ class Builder internal constructor() {
private var baseUrl: HttpUrl? = null
- // The default timeout is 1 minute.
- private var timeout: Duration = Duration.ofSeconds(60)
+ private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl.toHttpUrl() }
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
fun build(): OkHttpClient =
OkHttpClient(
okhttp3.OkHttpClient.Builder()
- .connectTimeout(timeout)
- .readTimeout(timeout)
- .writeTimeout(timeout)
- .callTimeout(if (timeout.seconds == 0L) timeout else timeout.plusSeconds(30))
+ .connectTimeout(timeout.connect())
+ .readTimeout(timeout.read())
+ .writeTimeout(timeout.write())
+ .callTimeout(timeout.request())
.proxy(proxy)
.build(),
- checkNotNull(baseUrl) { "`baseUrl` is required but was not set" },
+ checkRequired("baseUrl", baseUrl),
)
}
}
diff --git a/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt b/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt
index 6369d219..84b59f0f 100644
--- a/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt
+++ b/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt
@@ -6,32 +6,43 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.openlayer.api.client.OpenlayerClient
import com.openlayer.api.client.OpenlayerClientImpl
import com.openlayer.api.core.ClientOptions
+import com.openlayer.api.core.Timeout
import com.openlayer.api.core.http.Headers
import com.openlayer.api.core.http.QueryParams
import java.net.Proxy
import java.time.Clock
import java.time.Duration
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
class OpenlayerOkHttpClient private constructor() {
companion object {
+ /** Returns a mutable builder for constructing an instance of [OpenlayerOkHttpClient]. */
@JvmStatic fun builder() = Builder()
@JvmStatic fun fromEnv(): OpenlayerClient = builder().fromEnv().build()
}
- class Builder {
+ /** A builder for [OpenlayerOkHttpClient]. */
+ class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
- private var baseUrl: String = ClientOptions.PRODUCTION_URL
- // The default timeout for the client is 1 minute.
- private var timeout: Duration = Duration.ofSeconds(60)
+ private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
- fun baseUrl(baseUrl: String) = apply {
- clientOptions.baseUrl(baseUrl)
- this.baseUrl = baseUrl
+ fun baseUrl(baseUrl: String) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee
+ * that the SDK will work correctly when using an incompatible Jackson version.
+ */
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
@@ -118,7 +129,19 @@ class OpenlayerOkHttpClient private constructor() {
clientOptions.removeAllQueryParams(keys)
}
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply {
+ clientOptions.timeout(timeout)
+ this.timeout = timeout
+ }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
@@ -130,14 +153,22 @@ class OpenlayerOkHttpClient private constructor() {
fun apiKey(apiKey: String?) = apply { clientOptions.apiKey(apiKey) }
+ /** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
+ fun apiKey(apiKey: Optional) = apiKey(apiKey.getOrNull())
+
fun fromEnv() = apply { clientOptions.fromEnv() }
+ /**
+ * Returns an immutable instance of [OpenlayerClient].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
fun build(): OpenlayerClient =
OpenlayerClientImpl(
clientOptions
.httpClient(
OkHttpClient.builder()
- .baseUrl(baseUrl)
+ .baseUrl(clientOptions.baseUrl())
.timeout(timeout)
.proxy(proxy)
.build()
diff --git a/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClientAsync.kt b/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClientAsync.kt
index 3bd8ace4..ed7c1612 100644
--- a/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClientAsync.kt
+++ b/openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClientAsync.kt
@@ -6,32 +6,45 @@ import com.fasterxml.jackson.databind.json.JsonMapper
import com.openlayer.api.client.OpenlayerClientAsync
import com.openlayer.api.client.OpenlayerClientAsyncImpl
import com.openlayer.api.core.ClientOptions
+import com.openlayer.api.core.Timeout
import com.openlayer.api.core.http.Headers
import com.openlayer.api.core.http.QueryParams
import java.net.Proxy
import java.time.Clock
import java.time.Duration
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
class OpenlayerOkHttpClientAsync private constructor() {
companion object {
+ /**
+ * Returns a mutable builder for constructing an instance of [OpenlayerOkHttpClientAsync].
+ */
@JvmStatic fun builder() = Builder()
@JvmStatic fun fromEnv(): OpenlayerClientAsync = builder().fromEnv().build()
}
- class Builder {
+ /** A builder for [OpenlayerOkHttpClientAsync]. */
+ class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
- private var baseUrl: String = ClientOptions.PRODUCTION_URL
- // The default timeout for the client is 1 minute.
- private var timeout: Duration = Duration.ofSeconds(60)
+ private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
- fun baseUrl(baseUrl: String) = apply {
- clientOptions.baseUrl(baseUrl)
- this.baseUrl = baseUrl
+ fun baseUrl(baseUrl: String) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee
+ * that the SDK will work correctly when using an incompatible Jackson version.
+ */
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
@@ -118,7 +131,19 @@ class OpenlayerOkHttpClientAsync private constructor() {
clientOptions.removeAllQueryParams(keys)
}
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply {
+ clientOptions.timeout(timeout)
+ this.timeout = timeout
+ }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
@@ -130,14 +155,22 @@ class OpenlayerOkHttpClientAsync private constructor() {
fun apiKey(apiKey: String?) = apply { clientOptions.apiKey(apiKey) }
+ /** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
+ fun apiKey(apiKey: Optional) = apiKey(apiKey.getOrNull())
+
fun fromEnv() = apply { clientOptions.fromEnv() }
+ /**
+ * Returns an immutable instance of [OpenlayerClientAsync].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
fun build(): OpenlayerClientAsync =
OpenlayerClientAsyncImpl(
clientOptions
.httpClient(
OkHttpClient.builder()
- .baseUrl(baseUrl)
+ .baseUrl(clientOptions.baseUrl())
.timeout(timeout)
.proxy(proxy)
.build()
diff --git a/openlayer-java-core/build.gradle.kts b/openlayer-java-core/build.gradle.kts
index cbb56c6f..5e14f894 100644
--- a/openlayer-java-core/build.gradle.kts
+++ b/openlayer-java-core/build.gradle.kts
@@ -3,14 +3,28 @@ plugins {
id("openlayer.publish")
}
+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")
+ }
+}
+
dependencies {
- api("com.fasterxml.jackson.core:jackson-core:2.18.1")
- api("com.fasterxml.jackson.core:jackson-databind:2.18.1")
+ api("com.fasterxml.jackson.core:jackson-core:2.18.2")
+ api("com.fasterxml.jackson.core:jackson-databind:2.18.2")
+ api("com.google.errorprone:error_prone_annotations:2.33.0")
- implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.1")
- implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.1")
- implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.1")
- implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.1")
+ implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.2")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2")
implementation("org.apache.httpcomponents.core5:httpcore5:5.2.4")
implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1")
@@ -20,6 +34,7 @@ dependencies {
testImplementation("org.assertj:assertj-core:3.25.3")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3")
+ testImplementation("org.junit-pioneer:junit-pioneer:1.9.1")
testImplementation("org.mockito:mockito-core:5.14.2")
testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClient.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClient.kt
index b7d18d42..7697f0c8 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClient.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClient.kt
@@ -7,10 +7,35 @@ import com.openlayer.api.services.blocking.InferencePipelineService
import com.openlayer.api.services.blocking.ProjectService
import com.openlayer.api.services.blocking.StorageService
+/**
+ * A client for interacting with the Openlayer REST API synchronously. You can also switch to
+ * asynchronous execution via the [async] method.
+ *
+ * This client performs best when you create a single instance and reuse it for all interactions
+ * with the REST API. This is because each client holds its own connection pool and thread pools.
+ * Reusing connections and threads reduces latency and saves memory. The client also handles rate
+ * limiting per client. This means that creating and using multiple instances at the same time will
+ * not respect rate limits.
+ *
+ * The threads and connections that are held will be released automatically if they remain idle. But
+ * if you are writing an application that needs to aggressively release unused resources, then you
+ * may call [close].
+ */
interface OpenlayerClient {
+ /**
+ * Returns a version of this client that uses asynchronous execution.
+ *
+ * The returned client shares its resources, like its connection pool and thread pools, with
+ * this client.
+ */
fun async(): OpenlayerClientAsync
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
fun projects(): ProjectService
fun commits(): CommitService
@@ -18,4 +43,29 @@ interface OpenlayerClient {
fun inferencePipelines(): InferencePipelineService
fun storage(): StorageService
+
+ /**
+ * Closes this client, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client is long-lived and
+ * usually should not be synchronously closed via try-with-resources.
+ *
+ * It's also usually not necessary to call this method at all. the default HTTP client
+ * automatically releases threads and connections if they remain idle, but if you are writing an
+ * application that needs to aggressively release unused resources, then you may call this
+ * method.
+ */
+ fun close()
+
+ /** A view of [OpenlayerClient] that provides access to raw HTTP responses for each method. */
+ interface WithRawResponse {
+
+ fun projects(): ProjectService.WithRawResponse
+
+ fun commits(): CommitService.WithRawResponse
+
+ fun inferencePipelines(): InferencePipelineService.WithRawResponse
+
+ fun storage(): StorageService.WithRawResponse
+ }
}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsync.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsync.kt
index d9a6d7a1..cd90e546 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsync.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsync.kt
@@ -7,10 +7,35 @@ import com.openlayer.api.services.async.InferencePipelineServiceAsync
import com.openlayer.api.services.async.ProjectServiceAsync
import com.openlayer.api.services.async.StorageServiceAsync
+/**
+ * A client for interacting with the Openlayer REST API asynchronously. You can also switch to
+ * synchronous execution via the [sync] method.
+ *
+ * This client performs best when you create a single instance and reuse it for all interactions
+ * with the REST API. This is because each client holds its own connection pool and thread pools.
+ * Reusing connections and threads reduces latency and saves memory. The client also handles rate
+ * limiting per client. This means that creating and using multiple instances at the same time will
+ * not respect rate limits.
+ *
+ * The threads and connections that are held will be released automatically if they remain idle. But
+ * if you are writing an application that needs to aggressively release unused resources, then you
+ * may call [close].
+ */
interface OpenlayerClientAsync {
+ /**
+ * Returns a version of this client that uses synchronous execution.
+ *
+ * The returned client shares its resources, like its connection pool and thread pools, with
+ * this client.
+ */
fun sync(): OpenlayerClient
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
fun projects(): ProjectServiceAsync
fun commits(): CommitServiceAsync
@@ -18,4 +43,31 @@ interface OpenlayerClientAsync {
fun inferencePipelines(): InferencePipelineServiceAsync
fun storage(): StorageServiceAsync
+
+ /**
+ * Closes this client, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client is long-lived and
+ * usually should not be synchronously closed via try-with-resources.
+ *
+ * It's also usually not necessary to call this method at all. the default HTTP client
+ * automatically releases threads and connections if they remain idle, but if you are writing an
+ * application that needs to aggressively release unused resources, then you may call this
+ * method.
+ */
+ fun close()
+
+ /**
+ * A view of [OpenlayerClientAsync] that provides access to raw HTTP responses for each method.
+ */
+ interface WithRawResponse {
+
+ fun projects(): ProjectServiceAsync.WithRawResponse
+
+ fun commits(): CommitServiceAsync.WithRawResponse
+
+ fun inferencePipelines(): InferencePipelineServiceAsync.WithRawResponse
+
+ fun storage(): StorageServiceAsync.WithRawResponse
+ }
}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsyncImpl.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsyncImpl.kt
index 9335e18a..1a5e1785 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsyncImpl.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientAsyncImpl.kt
@@ -13,10 +13,7 @@ import com.openlayer.api.services.async.ProjectServiceAsyncImpl
import com.openlayer.api.services.async.StorageServiceAsync
import com.openlayer.api.services.async.StorageServiceAsyncImpl
-class OpenlayerClientAsyncImpl
-constructor(
- private val clientOptions: ClientOptions,
-) : OpenlayerClientAsync {
+class OpenlayerClientAsyncImpl(private val clientOptions: ClientOptions) : OpenlayerClientAsync {
private val clientOptionsWithUserAgent =
if (clientOptions.headers.names().contains("User-Agent")) clientOptions
@@ -29,6 +26,10 @@ constructor(
// Pass the original clientOptions so that this client sets its own User-Agent.
private val sync: OpenlayerClient by lazy { OpenlayerClientImpl(clientOptions) }
+ private val withRawResponse: OpenlayerClientAsync.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
private val projects: ProjectServiceAsync by lazy {
ProjectServiceAsyncImpl(clientOptionsWithUserAgent)
}
@@ -47,6 +48,8 @@ constructor(
override fun sync(): OpenlayerClient = sync
+ override fun withRawResponse(): OpenlayerClientAsync.WithRawResponse = withRawResponse
+
override fun projects(): ProjectServiceAsync = projects
override fun commits(): CommitServiceAsync = commits
@@ -54,4 +57,35 @@ constructor(
override fun inferencePipelines(): InferencePipelineServiceAsync = inferencePipelines
override fun storage(): StorageServiceAsync = storage
+
+ override fun close() = clientOptions.httpClient.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ OpenlayerClientAsync.WithRawResponse {
+
+ private val projects: ProjectServiceAsync.WithRawResponse by lazy {
+ ProjectServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val commits: CommitServiceAsync.WithRawResponse by lazy {
+ CommitServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val inferencePipelines: InferencePipelineServiceAsync.WithRawResponse by lazy {
+ InferencePipelineServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val storage: StorageServiceAsync.WithRawResponse by lazy {
+ StorageServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun projects(): ProjectServiceAsync.WithRawResponse = projects
+
+ override fun commits(): CommitServiceAsync.WithRawResponse = commits
+
+ override fun inferencePipelines(): InferencePipelineServiceAsync.WithRawResponse =
+ inferencePipelines
+
+ override fun storage(): StorageServiceAsync.WithRawResponse = storage
+ }
}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientImpl.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientImpl.kt
index 4577ea48..f11b7575 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientImpl.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/client/OpenlayerClientImpl.kt
@@ -13,10 +13,7 @@ import com.openlayer.api.services.blocking.ProjectServiceImpl
import com.openlayer.api.services.blocking.StorageService
import com.openlayer.api.services.blocking.StorageServiceImpl
-class OpenlayerClientImpl
-constructor(
- private val clientOptions: ClientOptions,
-) : OpenlayerClient {
+class OpenlayerClientImpl(private val clientOptions: ClientOptions) : OpenlayerClient {
private val clientOptionsWithUserAgent =
if (clientOptions.headers.names().contains("User-Agent")) clientOptions
@@ -29,6 +26,10 @@ constructor(
// Pass the original clientOptions so that this client sets its own User-Agent.
private val async: OpenlayerClientAsync by lazy { OpenlayerClientAsyncImpl(clientOptions) }
+ private val withRawResponse: OpenlayerClient.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
private val projects: ProjectService by lazy { ProjectServiceImpl(clientOptionsWithUserAgent) }
private val commits: CommitService by lazy { CommitServiceImpl(clientOptionsWithUserAgent) }
@@ -41,6 +42,8 @@ constructor(
override fun async(): OpenlayerClientAsync = async
+ override fun withRawResponse(): OpenlayerClient.WithRawResponse = withRawResponse
+
override fun projects(): ProjectService = projects
override fun commits(): CommitService = commits
@@ -48,4 +51,35 @@ constructor(
override fun inferencePipelines(): InferencePipelineService = inferencePipelines
override fun storage(): StorageService = storage
+
+ override fun close() = clientOptions.httpClient.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ OpenlayerClient.WithRawResponse {
+
+ private val projects: ProjectService.WithRawResponse by lazy {
+ ProjectServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val commits: CommitService.WithRawResponse by lazy {
+ CommitServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val inferencePipelines: InferencePipelineService.WithRawResponse by lazy {
+ InferencePipelineServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val storage: StorageService.WithRawResponse by lazy {
+ StorageServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun projects(): ProjectService.WithRawResponse = projects
+
+ override fun commits(): CommitService.WithRawResponse = commits
+
+ override fun inferencePipelines(): InferencePipelineService.WithRawResponse =
+ inferencePipelines
+
+ override fun storage(): StorageService.WithRawResponse = storage
+ }
}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/BaseDeserializer.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/BaseDeserializer.kt
index 0573e162..e624381e 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/BaseDeserializer.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/BaseDeserializer.kt
@@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.BeanProperty
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonDeserializer
-import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
@@ -18,7 +17,7 @@ abstract class BaseDeserializer(type: KClass) :
override fun createContextual(
context: DeserializationContext,
- property: BeanProperty?
+ property: BeanProperty?,
): JsonDeserializer {
return this
}
@@ -29,31 +28,17 @@ abstract class BaseDeserializer(type: KClass) :
protected abstract fun ObjectCodec.deserialize(node: JsonNode): T
- protected fun ObjectCodec.tryDeserialize(
- node: JsonNode,
- type: TypeReference,
- validate: (T) -> Unit = {}
- ): T? {
- return try {
- readValue(treeAsTokens(node), type).apply(validate)
- } catch (e: JsonMappingException) {
- null
- } catch (e: RuntimeException) {
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: TypeReference): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
null
}
- }
- protected fun ObjectCodec.tryDeserialize(
- node: JsonNode,
- type: JavaType,
- validate: (T) -> Unit = {}
- ): T? {
- return try {
- readValue(treeAsTokens(node), type).apply(validate)
- } catch (e: JsonMappingException) {
- null
- } catch (e: RuntimeException) {
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: JavaType): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
null
}
- }
}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Check.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Check.kt
new file mode 100644
index 00000000..2d154c84
--- /dev/null
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Check.kt
@@ -0,0 +1,96 @@
+@file:JvmName("Check")
+
+package com.openlayer.api.core
+
+import com.fasterxml.jackson.core.Version
+import com.fasterxml.jackson.core.util.VersionUtil
+
+fun checkRequired(name: String, condition: Boolean) =
+ check(condition) { "`$name` is required, but was not set" }
+
+fun checkRequired(name: String, value: T?): T =
+ checkNotNull(value) { "`$name` is required, but was not set" }
+
+@JvmSynthetic
+internal fun checkKnown(name: String, value: JsonField): T =
+ value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
+@JvmSynthetic
+internal fun checkKnown(name: String, value: MultipartField): T =
+ value.value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
+@JvmSynthetic
+internal fun checkLength(name: String, value: String, length: Int): String =
+ value.also {
+ check(it.length == length) { "`$name` must have length $length, but was ${it.length}" }
+ }
+
+@JvmSynthetic
+internal fun checkMinLength(name: String, value: String, minLength: Int): String =
+ value.also {
+ check(it.length >= minLength) {
+ if (minLength == 1) "`$name` must be non-empty, but was empty"
+ else "`$name` must have at least length $minLength, but was ${it.length}"
+ }
+ }
+
+@JvmSynthetic
+internal fun checkMaxLength(name: String, value: String, maxLength: Int): String =
+ value.also {
+ check(it.length <= maxLength) {
+ "`$name` must have at most length $maxLength, but was ${it.length}"
+ }
+ }
+
+@JvmSynthetic
+internal fun checkJacksonVersionCompatibility() {
+ val incompatibleJacksonVersions =
+ RUNTIME_JACKSON_VERSIONS.mapNotNull {
+ val badVersionReason = BAD_JACKSON_VERSIONS[it.toString()]
+ when {
+ it.majorVersion != MINIMUM_JACKSON_VERSION.majorVersion ->
+ it to "incompatible major version"
+ it.minorVersion < MINIMUM_JACKSON_VERSION.minorVersion ->
+ it to "minor version too low"
+ it.minorVersion == MINIMUM_JACKSON_VERSION.minorVersion &&
+ it.patchLevel < MINIMUM_JACKSON_VERSION.patchLevel ->
+ it to "patch version too low"
+ badVersionReason != null -> it to badVersionReason
+ else -> null
+ }
+ }
+ check(incompatibleJacksonVersions.isEmpty()) {
+ """
+This SDK requires a minimum Jackson version of $MINIMUM_JACKSON_VERSION, but the following incompatible Jackson versions were detected at runtime:
+
+${incompatibleJacksonVersions.asSequence().map { (version, incompatibilityReason) ->
+ "- `${version.toFullString().replace("/", ":")}` ($incompatibilityReason)"
+}.joinToString("\n")}
+
+This can happen if you are either:
+1. Directly depending on different Jackson versions
+2. Depending on some library that depends on different Jackson versions, potentially transitively
+
+Double-check that you are depending on compatible Jackson versions.
+
+See https://www.github.com/openlayer-ai/openlayer-java#jackson for more information.
+ """
+ .trimIndent()
+ }
+}
+
+private val MINIMUM_JACKSON_VERSION: Version = VersionUtil.parseVersion("2.13.4", null, null)
+private val BAD_JACKSON_VERSIONS: Map =
+ mapOf("2.18.1" to "due to https://github.com/FasterXML/jackson-databind/issues/4639")
+private val RUNTIME_JACKSON_VERSIONS: List =
+ listOf(
+ com.fasterxml.jackson.core.json.PackageVersion.VERSION,
+ com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jdk8.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jsr310.PackageVersion.VERSION,
+ com.fasterxml.jackson.module.kotlin.PackageVersion.VERSION,
+ )
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ClientOptions.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ClientOptions.kt
index 9dbd51c6..37bbbb54 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ClientOptions.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ClientOptions.kt
@@ -9,65 +9,107 @@ import com.openlayer.api.core.http.PhantomReachableClosingHttpClient
import com.openlayer.api.core.http.QueryParams
import com.openlayer.api.core.http.RetryingHttpClient
import java.time.Clock
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
class ClientOptions
private constructor(
private val originalHttpClient: HttpClient,
@get:JvmName("httpClient") val httpClient: HttpClient,
+ @get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean,
@get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
@get:JvmName("clock") val clock: Clock,
@get:JvmName("baseUrl") val baseUrl: String,
@get:JvmName("headers") val headers: Headers,
@get:JvmName("queryParams") val queryParams: QueryParams,
@get:JvmName("responseValidation") val responseValidation: Boolean,
+ @get:JvmName("timeout") val timeout: Timeout,
@get:JvmName("maxRetries") val maxRetries: Int,
- @get:JvmName("apiKey") val apiKey: String?,
+ private val apiKey: String?,
) {
+ init {
+ if (checkJacksonVersionCompatibility) {
+ checkJacksonVersionCompatibility()
+ }
+ }
+
+ fun apiKey(): Optional = Optional.ofNullable(apiKey)
+
fun toBuilder() = Builder().from(this)
companion object {
const val PRODUCTION_URL = "https://api.openlayer.com/v1"
+ /**
+ * Returns a mutable builder for constructing an instance of [ClientOptions].
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * ```
+ */
@JvmStatic fun builder() = Builder()
@JvmStatic fun fromEnv(): ClientOptions = builder().fromEnv().build()
}
- class Builder {
+ /** A builder for [ClientOptions]. */
+ class Builder internal constructor() {
private var httpClient: HttpClient? = null
+ private var checkJacksonVersionCompatibility: Boolean = true
private var jsonMapper: JsonMapper = jsonMapper()
private var clock: Clock = Clock.systemUTC()
private var baseUrl: String = PRODUCTION_URL
private var headers: Headers.Builder = Headers.builder()
private var queryParams: QueryParams.Builder = QueryParams.builder()
private var responseValidation: Boolean = false
+ private var timeout: Timeout = Timeout.default()
private var maxRetries: Int = 2
private var apiKey: String? = null
@JvmSynthetic
internal fun from(clientOptions: ClientOptions) = apply {
httpClient = clientOptions.originalHttpClient
+ checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
jsonMapper = clientOptions.jsonMapper
clock = clientOptions.clock
baseUrl = clientOptions.baseUrl
headers = clientOptions.headers.toBuilder()
queryParams = clientOptions.queryParams.toBuilder()
responseValidation = clientOptions.responseValidation
+ timeout = clientOptions.timeout
maxRetries = clientOptions.maxRetries
apiKey = clientOptions.apiKey
}
fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility
+ }
+
fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }
fun clock(clock: Clock) = apply { this.clock = clock }
fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl }
+ fun responseValidation(responseValidation: Boolean) = apply {
+ this.responseValidation = responseValidation
+ }
+
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
+
+ fun apiKey(apiKey: String?) = apply { this.apiKey = apiKey }
+
+ /** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
+ fun apiKey(apiKey: Optional) = apiKey(apiKey.getOrNull())
+
fun headers(headers: Headers) = apply {
this.headers.clear()
putAllHeaders(headers)
@@ -148,18 +190,27 @@ private constructor(
fun removeAllQueryParams(keys: Set) = apply { queryParams.removeAll(keys) }
- fun responseValidation(responseValidation: Boolean) = apply {
- this.responseValidation = responseValidation
- }
-
- fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
-
- fun apiKey(apiKey: String?) = apply { this.apiKey = apiKey }
-
- fun fromEnv() = apply { System.getenv("OPENLAYER_API_KEY")?.let { apiKey(it) } }
-
+ fun baseUrl(): String = baseUrl
+
+ fun fromEnv() = apply {
+ System.getenv("OPENLAYER_BASE_URL")?.let { baseUrl(it) }
+ System.getenv("OPENLAYER_API_KEY")?.let { apiKey(it) }
+ }
+
+ /**
+ * Returns an immutable instance of [ClientOptions].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * ```
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
fun build(): ClientOptions {
- checkNotNull(httpClient) { "`httpClient` is required but was not set" }
+ val httpClient = checkRequired("httpClient", httpClient)
val headers = Headers.builder()
val queryParams = QueryParams.builder()
@@ -179,20 +230,22 @@ private constructor(
queryParams.replaceAll(this.queryParams.build())
return ClientOptions(
- httpClient!!,
+ httpClient,
PhantomReachableClosingHttpClient(
RetryingHttpClient.builder()
- .httpClient(httpClient!!)
+ .httpClient(httpClient)
.clock(clock)
.maxRetries(maxRetries)
.build()
),
+ checkJacksonVersionCompatibility,
jsonMapper,
clock,
baseUrl,
headers.build(),
queryParams.build(),
responseValidation,
+ timeout,
maxRetries,
apiKey,
)
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/HttpRequestBodies.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/HttpRequestBodies.kt
deleted file mode 100644
index 8838f20c..00000000
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/HttpRequestBodies.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-@file:JvmName("HttpRequestBodies")
-
-package com.openlayer.api.core
-
-import com.fasterxml.jackson.databind.json.JsonMapper
-import com.openlayer.api.core.http.HttpRequestBody
-import com.openlayer.api.errors.OpenlayerException
-import java.io.ByteArrayOutputStream
-import java.io.OutputStream
-import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder
-
-@JvmSynthetic
-internal inline fun json(
- jsonMapper: JsonMapper,
- value: T,
-): HttpRequestBody {
- return object : HttpRequestBody {
- private var cachedBytes: ByteArray? = null
-
- private fun serialize(): ByteArray {
- if (cachedBytes != null) return cachedBytes!!
-
- val buffer = ByteArrayOutputStream()
- try {
- jsonMapper.writeValue(buffer, value)
- cachedBytes = buffer.toByteArray()
- return cachedBytes!!
- } catch (e: Exception) {
- throw OpenlayerException("Error writing request", e)
- }
- }
-
- override fun writeTo(outputStream: OutputStream) {
- outputStream.write(serialize())
- }
-
- override fun contentType(): String = "application/json"
-
- override fun contentLength(): Long {
- return serialize().size.toLong()
- }
-
- override fun repeatable(): Boolean = true
-
- override fun close() {}
- }
-}
-
-@JvmSynthetic
-internal fun multipartFormData(
- jsonMapper: JsonMapper,
- parts: Array?>
-): HttpRequestBody {
- val builder = MultipartEntityBuilder.create()
- parts.forEach { part ->
- if (part?.value != null) {
- when (part.value) {
- is JsonValue -> {
- val buffer = ByteArrayOutputStream()
- try {
- jsonMapper.writeValue(buffer, part.value)
- } catch (e: Exception) {
- throw OpenlayerException("Error serializing value to json", e)
- }
- builder.addBinaryBody(
- part.name,
- buffer.toByteArray(),
- part.contentType,
- part.filename
- )
- }
- is Boolean ->
- builder.addTextBody(
- part.name,
- if (part.value) "true" else "false",
- part.contentType
- )
- is Int -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- is Long -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- is Double -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- is ByteArray ->
- builder.addBinaryBody(part.name, part.value, part.contentType, part.filename)
- is String -> builder.addTextBody(part.name, part.value, part.contentType)
- is Enum -> builder.addTextBody(part.name, part.value.toString(), part.contentType)
- else ->
- throw IllegalArgumentException(
- "Unsupported content type: ${part.value::class.java.simpleName}"
- )
- }
- }
- }
- val entity = builder.build()
-
- return object : HttpRequestBody {
- override fun writeTo(outputStream: OutputStream) {
- try {
- return entity.writeTo(outputStream)
- } catch (e: Exception) {
- throw OpenlayerException("Error writing request", e)
- }
- }
-
- override fun contentType(): String = entity.contentType
-
- override fun contentLength(): Long = -1
-
- override fun repeatable(): Boolean = entity.isRepeatable
-
- override fun close() = entity.close()
- }
-}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ObjectMappers.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ObjectMappers.kt
index 2a22f38a..1438d8ea 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ObjectMappers.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/ObjectMappers.kt
@@ -3,23 +3,165 @@
package com.openlayer.api.core
import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.JsonParseException
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.SerializationFeature
-import com.fasterxml.jackson.databind.cfg.CoercionAction.Fail
-import com.fasterxml.jackson.databind.cfg.CoercionInputShape.Integer
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.cfg.CoercionAction
+import com.fasterxml.jackson.databind.cfg.CoercionInputShape
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.json.JsonMapper
+import com.fasterxml.jackson.databind.module.SimpleModule
+import com.fasterxml.jackson.databind.type.LogicalType
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
-import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
+import com.fasterxml.jackson.module.kotlin.kotlinModule
+import java.io.InputStream
+import java.time.DateTimeException
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoField
fun jsonMapper(): JsonMapper =
- jacksonMapperBuilder()
+ JsonMapper.builder()
+ .addModule(kotlinModule())
.addModule(Jdk8Module())
.addModule(JavaTimeModule())
+ .addModule(
+ SimpleModule()
+ .addSerializer(InputStreamSerializer)
+ .addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer())
+ )
+ .withCoercionConfig(LogicalType.Boolean) {
+ it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Integer) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Float) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Textual) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .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)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Collection) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Map) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.POJO) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ }
.serializationInclusion(JsonInclude.Include.NON_ABSENT)
.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
.disable(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
- .withCoercionConfig(String::class.java) { it.setCoercion(Integer, Fail) }
+ .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
+ .disable(MapperFeature.AUTO_DETECT_CREATORS)
+ .disable(MapperFeature.AUTO_DETECT_FIELDS)
+ .disable(MapperFeature.AUTO_DETECT_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_IS_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_SETTERS)
.build()
+
+/** A serializer that serializes [InputStream] to bytes. */
+private object InputStreamSerializer : BaseSerializer(InputStream::class) {
+
+ private fun readResolve(): Any = InputStreamSerializer
+
+ override fun serialize(
+ value: InputStream?,
+ gen: JsonGenerator?,
+ serializers: SerializerProvider?,
+ ) {
+ if (value == null) {
+ gen?.writeNull()
+ } else {
+ value.use { gen?.writeBinary(it.readBytes()) }
+ }
+ }
+}
+
+/**
+ * A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes.
+ */
+private class LenientLocalDateTimeDeserializer :
+ StdDeserializer(LocalDateTime::class.java) {
+
+ companion object {
+
+ private val DATE_TIME_FORMATTERS =
+ listOf(
+ DateTimeFormatter.ISO_LOCAL_DATE_TIME,
+ DateTimeFormatter.ISO_LOCAL_DATE,
+ DateTimeFormatter.ISO_ZONED_DATE_TIME,
+ )
+ }
+
+ override fun logicalType(): LogicalType = LogicalType.DateTime
+
+ override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime {
+ val exceptions = mutableListOf()
+
+ for (formatter in DATE_TIME_FORMATTERS) {
+ try {
+ val temporal = formatter.parse(p.text)
+
+ return when {
+ !temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
+ LocalDate.from(temporal).atStartOfDay()
+ !temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
+ LocalDateTime.from(temporal)
+ else -> ZonedDateTime.from(temporal).toLocalDateTime()
+ }
+ } catch (e: DateTimeException) {
+ exceptions.add(e)
+ }
+ }
+
+ throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply {
+ exceptions.forEach { addSuppressed(it) }
+ }
+ }
+}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Params.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Params.kt
new file mode 100644
index 00000000..62cbb730
--- /dev/null
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Params.kt
@@ -0,0 +1,16 @@
+package com.openlayer.api.core
+
+import com.openlayer.api.core.http.Headers
+import com.openlayer.api.core.http.QueryParams
+
+/** An interface representing parameters passed to a service method. */
+interface Params {
+ /** The full set of headers in the parameters, including both fixed and additional headers. */
+ fun _headers(): Headers
+
+ /**
+ * The full set of query params in the parameters, including both fixed and additional query
+ * params.
+ */
+ fun _queryParams(): QueryParams
+}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/PrepareRequest.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/PrepareRequest.kt
new file mode 100644
index 00000000..adf85bd8
--- /dev/null
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/PrepareRequest.kt
@@ -0,0 +1,24 @@
+@file:JvmName("PrepareRequest")
+
+package com.openlayer.api.core
+
+import com.openlayer.api.core.http.HttpRequest
+import java.util.concurrent.CompletableFuture
+
+@JvmSynthetic
+internal fun HttpRequest.prepare(clientOptions: ClientOptions, params: Params): HttpRequest =
+ toBuilder()
+ .putAllQueryParams(clientOptions.queryParams)
+ .replaceAllQueryParams(params._queryParams())
+ .putAllHeaders(clientOptions.headers)
+ .replaceAllHeaders(params._headers())
+ .build()
+
+@JvmSynthetic
+internal fun HttpRequest.prepareAsync(
+ clientOptions: ClientOptions,
+ params: Params,
+): CompletableFuture =
+ // This async version exists to make it easier to add async specific preparation logic in the
+ // future.
+ CompletableFuture.completedFuture(prepare(clientOptions, params))
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/RequestOptions.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/RequestOptions.kt
index 2b862a37..5ca3fddb 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/RequestOptions.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/RequestOptions.kt
@@ -2,17 +2,7 @@ package com.openlayer.api.core
import java.time.Duration
-class RequestOptions
-private constructor(
- val responseValidation: Boolean?,
- val timeout: Duration?,
-) {
- fun applyDefaults(options: RequestOptions): RequestOptions {
- return RequestOptions(
- responseValidation = this.responseValidation ?: options.responseValidation,
- timeout = this.timeout ?: options.timeout,
- )
- }
+class RequestOptions private constructor(val responseValidation: Boolean?, val timeout: Timeout?) {
companion object {
@@ -20,21 +10,37 @@ private constructor(
@JvmStatic fun none() = NONE
+ @JvmSynthetic
+ internal fun from(clientOptions: ClientOptions): RequestOptions =
+ builder()
+ .responseValidation(clientOptions.responseValidation)
+ .timeout(clientOptions.timeout)
+ .build()
+
@JvmStatic fun builder() = Builder()
}
- class Builder {
+ fun applyDefaults(options: RequestOptions): RequestOptions =
+ RequestOptions(
+ responseValidation = responseValidation ?: options.responseValidation,
+ timeout =
+ if (options.timeout != null && timeout != null) timeout.assign(options.timeout)
+ else timeout ?: options.timeout,
+ )
+
+ class Builder internal constructor() {
+
private var responseValidation: Boolean? = null
- private var timeout: Duration? = null
+ private var timeout: Timeout? = null
fun responseValidation(responseValidation: Boolean) = apply {
this.responseValidation = responseValidation
}
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
- fun build(): RequestOptions {
- return RequestOptions(responseValidation, timeout)
- }
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
+
+ fun build(): RequestOptions = RequestOptions(responseValidation, timeout)
}
}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Timeout.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Timeout.kt
new file mode 100644
index 00000000..e07cad54
--- /dev/null
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Timeout.kt
@@ -0,0 +1,167 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.openlayer.api.core
+
+import java.time.Duration
+import java.util.Objects
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+
+/** A class containing timeouts for various processing phases of a request. */
+class Timeout
+private constructor(
+ private val connect: Duration?,
+ private val read: Duration?,
+ private val write: Duration?,
+ private val request: Duration?,
+) {
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(): Duration = connect ?: Duration.ofMinutes(1)
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(): Duration = read ?: request()
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(): Duration = write ?: request()
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as well
+ * as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(): Duration = request ?: Duration.ofMinutes(1)
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ @JvmStatic fun default() = builder().build()
+
+ /** Returns a mutable builder for constructing an instance of [Timeout]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [Timeout]. */
+ class Builder internal constructor() {
+
+ private var connect: Duration? = null
+ private var read: Duration? = null
+ private var write: Duration? = null
+ private var request: Duration? = null
+
+ @JvmSynthetic
+ internal fun from(timeout: Timeout) = apply {
+ connect = timeout.connect
+ read = timeout.read
+ write = timeout.write
+ request = timeout.request
+ }
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(connect: Duration?) = apply { this.connect = connect }
+
+ /** Alias for calling [Builder.connect] with `connect.orElse(null)`. */
+ fun connect(connect: Optional) = connect(connect.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(read: Duration?) = apply { this.read = read }
+
+ /** Alias for calling [Builder.read] with `read.orElse(null)`. */
+ fun read(read: Optional) = read(read.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(write: Duration?) = apply { this.write = write }
+
+ /** Alias for calling [Builder.write] with `write.orElse(null)`. */
+ fun write(write: Optional) = write(write.getOrNull())
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as
+ * well as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(request: Duration?) = apply { this.request = request }
+
+ /** Alias for calling [Builder.request] with `request.orElse(null)`. */
+ fun request(request: Optional) = request(request.getOrNull())
+
+ /**
+ * Returns an immutable instance of [Timeout].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): Timeout = Timeout(connect, read, write, request)
+ }
+
+ @JvmSynthetic
+ internal fun assign(target: Timeout): Timeout =
+ target
+ .toBuilder()
+ .apply {
+ connect?.let(this::connect)
+ read?.let(this::read)
+ write?.let(this::write)
+ request?.let(this::request)
+ }
+ .build()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return /* spotless:off */ other is Timeout && connect == other.connect && read == other.read && write == other.write && request == other.request /* spotless:on */
+ }
+
+ override fun hashCode(): Int = /* spotless:off */ Objects.hash(connect, read, write, request) /* spotless:on */
+
+ override fun toString() =
+ "Timeout{connect=$connect, read=$read, write=$write, request=$request}"
+}
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Utils.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Utils.kt
index 7c5ab9d7..4482ae3d 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Utils.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Utils.kt
@@ -16,11 +16,77 @@ internal fun List.toImmutable(): List =
@JvmSynthetic
internal fun Map.toImmutable(): Map =
- if (isEmpty()) Collections.emptyMap() else Collections.unmodifiableMap(toMap())
+ if (isEmpty()) immutableEmptyMap() else Collections.unmodifiableMap(toMap())
+
+@JvmSynthetic internal fun immutableEmptyMap(): Map = Collections.emptyMap()
@JvmSynthetic
internal fun , V> SortedMap.toImmutable(): SortedMap =
if (isEmpty()) Collections.emptySortedMap()
else Collections.unmodifiableSortedMap(toSortedMap(comparator()))
+/**
+ * Returns all elements that yield the largest value for the given function, or an empty list if
+ * there are zero elements.
+ *
+ * This is similar to [Sequence.maxByOrNull] except it returns _all_ elements that yield the largest
+ * value; not just the first one.
+ */
+@JvmSynthetic
+internal fun > Sequence.allMaxBy(selector: (T) -> R): List {
+ var maxValue: R? = null
+ val maxElements = mutableListOf()
+
+ val iterator = iterator()
+ while (iterator.hasNext()) {
+ val element = iterator.next()
+ val value = selector(element)
+ if (maxValue == null || value > maxValue) {
+ maxValue = value
+ maxElements.clear()
+ maxElements.add(element)
+ } else if (value == maxValue) {
+ maxElements.add(element)
+ }
+ }
+
+ return maxElements
+}
+
+/**
+ * Returns whether [this] is equal to [other].
+ *
+ * This differs from [Object.equals] because it also deeply equates arrays based on their contents,
+ * even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic
+internal infix fun Any?.contentEquals(other: Any?): Boolean =
+ arrayOf(this).contentDeepEquals(arrayOf(other))
+
+/**
+ * Returns a hash of the given sequence of [values].
+ *
+ * This differs from [java.util.Objects.hash] because it also deeply hashes arrays based on their
+ * contents, even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic internal fun contentHash(vararg values: Any?): Int = values.contentDeepHashCode()
+
+/**
+ * Returns a [String] representation of [this].
+ *
+ * This differs from [Object.toString] because it also deeply stringifies arrays based on their
+ * contents, even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic
+internal fun Any?.contentToString(): String {
+ var string = arrayOf(this).contentDeepToString()
+ if (string.startsWith('[')) {
+ string = string.substring(1)
+ }
+ if (string.endsWith(']')) {
+ string = string.substring(0, string.length - 1)
+ }
+ return string
+}
+
internal interface Enum
diff --git a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt
index fc2653eb..88f25c14 100644
--- a/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt
+++ b/openlayer-java-core/src/main/kotlin/com/openlayer/api/core/Values.kt
@@ -1,8 +1,6 @@
package com.openlayer.api.core
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside
-import com.fasterxml.jackson.annotation.JsonAutoDetect
-import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.core.JsonGenerator
@@ -27,68 +25,145 @@ import com.fasterxml.jackson.databind.node.JsonNodeType.POJO
import com.fasterxml.jackson.databind.node.JsonNodeType.STRING
import com.fasterxml.jackson.databind.ser.std.NullSerializer
import com.openlayer.api.errors.OpenlayerInvalidDataException
-import java.nio.charset.Charset
+import java.io.InputStream
import java.util.Objects
import java.util.Optional
-import org.apache.hc.core5.http.ContentType
+/**
+ * A class representing a serializable JSON field.
+ *
+ * It can either be a [KnownValue] value of type [T], matching the type the SDK expects, or an
+ * arbitrary JSON value that bypasses the type system (via [JsonValue]).
+ */
@JsonDeserialize(using = JsonField.Deserializer::class)
sealed class JsonField {
+ /**
+ * Returns whether this field is missing, which means it will be omitted from the serialized
+ * JSON entirely.
+ */
fun isMissing(): Boolean = this is JsonMissing
+ /** Whether this field is explicitly set to `null`. */
fun isNull(): Boolean = this is JsonNull
- fun asKnown(): Optional =
- when (this) {
- is KnownValue -> Optional.of(value)
- else -> Optional.empty()
- }
+ /**
+ * Returns an [Optional] containing this field's "known" value, meaning it matches the type the
+ * SDK expects, or an empty [Optional] if this field contains an arbitrary [JsonValue].
+ *
+ * This is the opposite of [asUnknown].
+ */
+ fun asKnown():
+ Optional<
+ // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that.
+ @UnsafeVariance
+ T
+ > = Optional.ofNullable((this as? KnownValue)?.value)
/**
- * If the "known" value (i.e. matching the type that the SDK expects) is returned by the API
- * then this method will return an empty `Optional`, otherwise the returned `Optional` is given
- * a `JsonValue`.
+ * Returns an [Optional] containing this field's arbitrary [JsonValue], meaning it mismatches
+ * the type the SDK expects, or an empty [Optional] if this field contains a "known" value.
+ *
+ * This is the opposite of [asKnown].
*/
- fun asUnknown(): Optional =
- when (this) {
- is JsonValue -> Optional.of(this)
- else -> Optional.empty()
- }
+ fun asUnknown(): Optional = Optional.ofNullable(this as? JsonValue)
+ /**
+ * Returns an [Optional] containing this field's boolean value, or an empty [Optional] if it
+ * doesn't contain a boolean.
+ *
+ * This method checks for both a [KnownValue] containing a boolean and for [JsonBoolean].
+ */
fun asBoolean(): Optional =
when (this) {
is JsonBoolean -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Boolean)
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's numerical value, or an empty [Optional] if it
+ * doesn't contain a number.
+ *
+ * This method checks for both a [KnownValue] containing a number and for [JsonNumber].
+ */
fun asNumber(): Optional =
when (this) {
is JsonNumber -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Number)
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's string value, or an empty [Optional] if it
+ * doesn't contain a string.
+ *
+ * This method checks for both a [KnownValue] containing a string and for [JsonString].
+ */
fun asString(): Optional =
when (this) {
is JsonString -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? String)
else -> Optional.empty()
}
fun asStringOrThrow(): String =
- when (this) {
- is JsonString -> value
- else -> throw OpenlayerInvalidDataException("Value is not a string")
- }
+ asString().orElseThrow { OpenlayerInvalidDataException("Value is not a string") }
+ /**
+ * Returns an [Optional] containing this field's list value, or an empty [Optional] if it
+ * doesn't contain a list.
+ *
+ * This method checks for both a [KnownValue] containing a list and for [JsonArray].
+ */
fun asArray(): Optional> =
when (this) {
is JsonArray -> Optional.of(values)
+ is KnownValue ->
+ Optional.ofNullable(
+ (value as? List<*>)?.map {
+ try {
+ JsonValue.from(it)
+ } catch (e: IllegalArgumentException) {
+ // The known value is a list, but not all values are convertible to
+ // `JsonValue`.
+ return Optional.empty()
+ }
+ }
+ )
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's map value, or an empty [Optional] if it doesn't
+ * contain a map.
+ *
+ * This method checks for both a [KnownValue] containing a map and for [JsonObject].
+ */
fun asObject(): Optional