diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 94046873..a211506d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,29 +1,34 @@
name: CI
on:
push:
- branches:
- - main
+ branches-ignore:
+ - 'generated'
+ - 'codegen/**'
+ - 'integrated/**'
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
pull_request:
- branches:
- - main
- - next
+ branches-ignore:
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
jobs:
lint:
+ timeout-minutes: 15
name: lint
- runs-on: ubuntu-latest
-
+ runs-on: ${{ github.repository == 'stainless-sdks/hanzo-ai-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Set up Java
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: temurin
java-version: |
8
- 17
+ 21
cache: gradle
- name: Set up Gradle
@@ -31,20 +36,64 @@ jobs:
- name: Run lints
run: ./scripts/lint
+
+ build:
+ timeout-minutes: 15
+ name: build
+ permissions:
+ contents: read
+ id-token: write
+ runs-on: ${{ github.repository == 'stainless-sdks/hanzo-ai-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Set up Java
+ uses: actions/setup-java@v5
+ with:
+ distribution: temurin
+ java-version: |
+ 8
+ 21
+ cache: gradle
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Build SDK
+ run: ./scripts/build
+
+ - name: Get GitHub OIDC Token
+ if: github.repository == 'stainless-sdks/hanzo-ai-java'
+ id: github-oidc
+ uses: actions/github-script@v8
+ with:
+ script: core.setOutput('github_token', await core.getIDToken());
+
+ - name: Build and upload Maven artifacts
+ if: github.repository == 'stainless-sdks/hanzo-ai-java'
+ env:
+ URL: https://pkg.stainless.com/s
+ AUTH: ${{ steps.github-oidc.outputs.github_token }}
+ SHA: ${{ github.sha }}
+ PROJECT: hanzo-ai-java
+ run: ./scripts/upload-artifacts
test:
+ timeout-minutes: 15
name: test
- runs-on: ubuntu-latest
-
+ runs-on: ${{ github.repository == 'stainless-sdks/hanzo-ai-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Set up Java
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: temurin
java-version: |
8
- 17
+ 21
cache: gradle
- name: Set up Gradle
diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml
index 32aaa7ad..a89fe023 100644
--- a/.github/workflows/publish-sonatype.yml
+++ b/.github/workflows/publish-sonatype.yml
@@ -14,15 +14,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Set up Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v5
with:
distribution: temurin
java-version: |
8
- 17
+ 21
cache: gradle
- name: Set up Gradle
@@ -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.HANZO_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.HANZO_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
index 6de875a6..272e3ac8 100644
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'hanzoai/java-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Check release environment
run: |
diff --git a/.gitignore b/.gitignore
index 4e81838d..b1346e6d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,6 @@
.gradle
.idea
.kotlin
-build
+build/
codegen.log
kls_database.db
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f14b480a..aaf968a1 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.1.0-alpha.2"
+ ".": "0.1.0-alpha.3"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index dbc44ca8..e829d1ef 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 188
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hanzo-industries-inc%2FHanzo-AI-45310ce8b457cd9d29707322ab26acd87a4b64b286d9fe804bd7b23b89067b77.yml
-openapi_spec_hash: fb26cd549e0fe94f333db686235564fd
-config_hash: 830747463ff4d018b5633ce511e88558
+configured_endpoints: 187
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/hanzo-industries-inc%2Fhanzo-ai-971e209e1a47262e17483e5ac0d91e8013d10237ac89d6392ffabb5912ce83b9.yml
+openapi_spec_hash: 029e800ed42f138d5b63bb4f40181c55
+config_hash: e927bafd76a1eace11894efc3517d245
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a34706be..cbd19834 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,79 @@
# Changelog
+## 0.1.0-alpha.3 (2026-01-26)
+
+Full Changelog: [v0.1.0-alpha.2...v0.1.0-alpha.3](https://github.com/hanzoai/java-sdk/compare/v0.1.0-alpha.2...v0.1.0-alpha.3)
+
+### ā 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
+
+* **api:** api update ([589ce7a](https://github.com/hanzoai/java-sdk/commit/589ce7af97f7b1fcea2519ae75f184ea7a6529ff))
+* **api:** api update ([5997aeb](https://github.com/hanzoai/java-sdk/commit/5997aeb3a12a1dbb0e628c3ae2e2b73d6f5c0391))
+* **api:** api update ([bb22539](https://github.com/hanzoai/java-sdk/commit/bb225395cd286571c72d6ad48582d0f5b54b9db9))
+* **api:** api update ([#9](https://github.com/hanzoai/java-sdk/issues/9)) ([a0ab87a](https://github.com/hanzoai/java-sdk/commit/a0ab87ace8769fc2320a22cf1bd2d259ede567ae))
+* **client:** add enum validation method ([ebf7b97](https://github.com/hanzoai/java-sdk/commit/ebf7b9734d07621d28d394281160823dea9c8de3))
+* **client:** allow providing some params positionally ([4f35e18](https://github.com/hanzoai/java-sdk/commit/4f35e180123ecba96578a2eab86af922115cd005))
+* **client:** expose request body setter and getter ([#16](https://github.com/hanzoai/java-sdk/issues/16)) ([69c2c6c](https://github.com/hanzoai/java-sdk/commit/69c2c6cd26d41cd1e0a5012398cd8ba54a7617ca))
+* **client:** extract auto pagination to shared classes ([e735ee9](https://github.com/hanzoai/java-sdk/commit/e735ee9fb131714101989c4d8a724399932e8026))
+* **client:** make datetime deserialization more lenient ([#15](https://github.com/hanzoai/java-sdk/issues/15)) ([d1cf89d](https://github.com/hanzoai/java-sdk/commit/d1cf89db05f61aa38c0a8d274ba3ee77d17e0341))
+* **client:** make union deserialization more robust ([#14](https://github.com/hanzoai/java-sdk/issues/14)) ([ebf7b97](https://github.com/hanzoai/java-sdk/commit/ebf7b9734d07621d28d394281160823dea9c8de3))
+* **client:** support setting base URL via env var ([7b40d13](https://github.com/hanzoai/java-sdk/commit/7b40d13ad642b0db4f872cf54cb0e75a8fb5fc1c))
+
+
+### Bug Fixes
+
+* **client:** add missing convenience methods ([afd652a](https://github.com/hanzoai/java-sdk/commit/afd652a14b550ae68fc192379c2649f3dea63601))
+* **client:** bump to better jackson version ([21f5b79](https://github.com/hanzoai/java-sdk/commit/21f5b79e8a12fb75bb6ea91fc46173f4fed1fc8f))
+* **client:** don't call `validate()` during deserialization if we don't have to ([#11](https://github.com/hanzoai/java-sdk/issues/11)) ([e5af84a](https://github.com/hanzoai/java-sdk/commit/e5af84a40d2f50556adcb9b54d088ff4482c81c7))
+* **client:** limit json deserialization coercion ([#12](https://github.com/hanzoai/java-sdk/issues/12)) ([a476286](https://github.com/hanzoai/java-sdk/commit/a4762869b0ad34a0212ed4ea2fc00cce3a020487))
+* **client:** return `Optional<T>` instead of `Optional extends T>` ([#19](https://github.com/hanzoai/java-sdk/issues/19)) ([becb670](https://github.com/hanzoai/java-sdk/commit/becb670d92fab9e105289da811f8b2ce3c63d776))
+* pluralize `list` response variables ([#13](https://github.com/hanzoai/java-sdk/issues/13)) ([a7a0b4a](https://github.com/hanzoai/java-sdk/commit/a7a0b4a6b847da6df6b459af2c149c7569ad9e0c))
+
+
+### Performance Improvements
+
+* **client:** cached parsed type in `HttpResponseFor` ([#17](https://github.com/hanzoai/java-sdk/issues/17)) ([91c37b1](https://github.com/hanzoai/java-sdk/commit/91c37b152243864b852b3ab04c05ab0aadf28912))
+* **internal:** improve compilation+test speed ([668e12a](https://github.com/hanzoai/java-sdk/commit/668e12a649f917543d6a44810cdce0f035993afe))
+
+
+### Chores
+
+* **ci:** add timeout thresholds for CI jobs ([2711202](https://github.com/hanzoai/java-sdk/commit/271120209107485ef828f66150c6b32c14db9d2f))
+* **ci:** only use depot for staging repos ([76c8fa1](https://github.com/hanzoai/java-sdk/commit/76c8fa1b7cc7a685bbc0326773579dee3120a678))
+* **client:** remove unnecessary json state from some query param classes ([ebf7b97](https://github.com/hanzoai/java-sdk/commit/ebf7b9734d07621d28d394281160823dea9c8de3))
+* configure new SDK language ([4e8ff89](https://github.com/hanzoai/java-sdk/commit/4e8ff890138cf98a46038bf5ca5484820136929a))
+* **docs:** grammar improvements ([ccd164e](https://github.com/hanzoai/java-sdk/commit/ccd164eaddd08e28deb6dc7934e27b75febee968))
+* **internal:** add invalid json deserialization tests ([ebf7b97](https://github.com/hanzoai/java-sdk/commit/ebf7b9734d07621d28d394281160823dea9c8de3))
+* **internal:** add json roundtripping tests ([ebf7b97](https://github.com/hanzoai/java-sdk/commit/ebf7b9734d07621d28d394281160823dea9c8de3))
+* **internal:** codegen related update ([f5151f7](https://github.com/hanzoai/java-sdk/commit/f5151f714c25e8331f9da13e5f3cf0169d2c1bd5))
+* **internal:** codegen related update ([0b6be6f](https://github.com/hanzoai/java-sdk/commit/0b6be6fc53010ddef052f0142400aa2599225a26))
+* **internal:** expand CI branch coverage ([1eddeb1](https://github.com/hanzoai/java-sdk/commit/1eddeb17b64eb80f6db6969d181c8f48ec9a08a6))
+* **internal:** java 17 -> 21 on ci ([2bcbac5](https://github.com/hanzoai/java-sdk/commit/2bcbac55db86820f67047c1be46898513970bee8))
+* **internal:** reduce CI branch coverage ([c8a88d7](https://github.com/hanzoai/java-sdk/commit/c8a88d7303cd41aada70dea065bb8a51522c3373))
+* **internal:** remove flaky `-Xbackend-threads=0` option ([2548d25](https://github.com/hanzoai/java-sdk/commit/2548d2575aaab0c2dbcf87b9df3e9d04cd2f9f25))
+* **internal:** swap from `getNullable` to `getOptional` ([#18](https://github.com/hanzoai/java-sdk/issues/18)) ([b4a0c8b](https://github.com/hanzoai/java-sdk/commit/b4a0c8b295e7216dd22f472b4ee4198664f7dad0))
+* **internal:** update java toolchain ([5c62cf3](https://github.com/hanzoai/java-sdk/commit/5c62cf3a190d7fc3d2c0def9b589c7dceb0bb025))
+* **internal:** use `byteInputStream()` in tests ([afd652a](https://github.com/hanzoai/java-sdk/commit/afd652a14b550ae68fc192379c2649f3dea63601))
+
+
+### Documentation
+
+* add comments to `JsonField` classes ([becb670](https://github.com/hanzoai/java-sdk/commit/becb670d92fab9e105289da811f8b2ce3c63d776))
+* **client:** update jackson compat error message ([b2310d3](https://github.com/hanzoai/java-sdk/commit/b2310d32ce73c02a5942f04fbfc66d38c6c371d7))
+* document how to forcibly omit required field ([4dc5520](https://github.com/hanzoai/java-sdk/commit/4dc5520842ee6f01676aa386177d8924430fc3fa))
+* explain http client customization ([e43d638](https://github.com/hanzoai/java-sdk/commit/e43d6381ec86c1c7372e03fede163b2bd4fa673a))
+* explain jackson compat in readme ([b62faed](https://github.com/hanzoai/java-sdk/commit/b62faed4882e4f2a39014a11fda5297c09269a07))
+* swap examples used in readme ([#20](https://github.com/hanzoai/java-sdk/issues/20)) ([4dc5520](https://github.com/hanzoai/java-sdk/commit/4dc5520842ee6f01676aa386177d8924430fc3fa))
+* update documentation links to be more uniform ([c878297](https://github.com/hanzoai/java-sdk/commit/c878297f959b3215eacb52b1579d4dc0eae1f79b))
+
## 0.1.0-alpha.2 (2025-03-27)
Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/hanzoai/java-sdk/compare/v0.1.0-alpha.1...v0.1.0-alpha.2)
diff --git a/LICENSE b/LICENSE
index 614d2f02..ff91a2c8 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2025 Hanzo
+ Copyright 2026 Hanzo
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 f4d674b8..54ce185a 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
-[](https://central.sonatype.com/artifact/ai.hanzo.api/hanzo-java/0.1.0-alpha.2)
-[](https://javadoc.io/doc/ai.hanzo.api/hanzo-java/0.1.0-alpha.2)
+[](https://central.sonatype.com/artifact/ai.hanzo.api/hanzo-java/0.1.0-alpha.3)
+[](https://javadoc.io/doc/ai.hanzo.api/hanzo-java/0.1.0-alpha.3)
@@ -13,9 +13,18 @@ The Hanzo Java SDK is similar to the Hanzo Kotlin SDK but with minor differences
It is generated with [Stainless](https://www.stainless.com/).
+## MCP Server
+
+Use the Hanzo MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.
+
+[](https://cursor.com/en-US/install-mcp?name=hanzoai-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImhhbnpvYWktbWNwIl19)
+[](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22hanzoai-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22hanzoai-mcp%22%5D%7D)
+
+> Note: You may need to set environment variables in your MCP client.
+
-The REST API documentation can be found on [docs.hanzo.ai](https://docs.hanzo.ai). Javadocs are also available on [javadoc.io](https://javadoc.io/doc/ai.hanzo.api/hanzo-java/0.1.0-alpha.2).
+The REST API documentation can be found on [docs.hanzo.ai](https://docs.hanzo.ai). Javadocs are available on [javadoc.io](https://javadoc.io/doc/ai.hanzo.api/hanzo-java/0.1.0-alpha.3).
@@ -26,7 +35,7 @@ The REST API documentation can be found on [docs.hanzo.ai](https://docs.hanzo.ai
### Gradle
```kotlin
-implementation("ai.hanzo.api:hanzo-java:0.1.0-alpha.2")
+implementation("ai.hanzo.api:hanzo-java:0.1.0-alpha.3")
```
### Maven
@@ -35,7 +44,7 @@ implementation("ai.hanzo.api:hanzo-java:0.1.0-alpha.2")
ai.hanzo.api
hanzo-java
- 0.1.0-alpha.2
+ 0.1.0-alpha.3
```
@@ -53,7 +62,8 @@ import ai.hanzo.api.client.okhttp.HanzoOkHttpClient;
import ai.hanzo.api.models.ClientGetHomeParams;
import ai.hanzo.api.models.ClientGetHomeResponse;
-// Configures using the `HANZO_API_KEY` environment variable
+// Configures using the `hanzo.apiKey` and `hanzo.baseUrl` system properties
+// Or configures using the `HANZO_API_KEY` and `HANZO_BASE_URL` environment variables
HanzoClient client = HanzoOkHttpClient.fromEnv();
ClientGetHomeResponse response = client.getHome();
@@ -61,13 +71,14 @@ ClientGetHomeResponse response = client.getHome();
## Client configuration
-Configure the client using environment variables:
+Configure the client using system properties or environment variables:
```java
import ai.hanzo.api.client.HanzoClient;
import ai.hanzo.api.client.okhttp.HanzoOkHttpClient;
-// Configures using the `HANZO_API_KEY` environment variable
+// Configures using the `hanzo.apiKey` and `hanzo.baseUrl` system properties
+// Or configures using the `HANZO_API_KEY` and `HANZO_BASE_URL` environment variables
HanzoClient client = HanzoOkHttpClient.fromEnv();
```
@@ -89,7 +100,8 @@ import ai.hanzo.api.client.HanzoClient;
import ai.hanzo.api.client.okhttp.HanzoOkHttpClient;
HanzoClient client = HanzoOkHttpClient.builder()
- // Configures using the `HANZO_API_KEY` environment variable
+ // Configures using the `hanzo.apiKey` and `hanzo.baseUrl` system properties
+ // Or configures using the `HANZO_API_KEY` and `HANZO_BASE_URL` environment variables
.fromEnv()
.apiKey("My API Key")
.build();
@@ -97,14 +109,32 @@ HanzoClient client = HanzoOkHttpClient.builder()
See this table for the available options:
-| Setter | Environment variable | Required | Default value |
-| -------- | -------------------- | -------- | ------------- |
-| `apiKey` | `HANZO_API_KEY` | true | - |
+| Setter | System property | Environment variable | Required | Default value |
+| --------- | --------------- | -------------------- | -------- | ------------------------ |
+| `apiKey` | `hanzo.apiKey` | `HANZO_API_KEY` | true | - |
+| `baseUrl` | `hanzo.baseUrl` | `HANZO_BASE_URL` | true | `"https://api.hanzo.ai"` |
+
+System properties take precedence over environment variables.
> [!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.
+### Modifying configuration
+
+To temporarily use a modified client configuration, while reusing the same connection and thread pools, call `withOptions()` on any client or service:
+
+```java
+import ai.hanzo.api.client.HanzoClient;
+
+HanzoClient clientWithOptions = client.withOptions(optionsBuilder -> {
+ optionsBuilder.baseUrl("https://example.com");
+ optionsBuilder.maxRetries(42);
+});
+```
+
+The `withOptions()` method does not affect the original client or service.
+
## Requests and responses
To send a request to the Hanzo 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.
@@ -130,7 +160,8 @@ import ai.hanzo.api.models.ClientGetHomeParams;
import ai.hanzo.api.models.ClientGetHomeResponse;
import java.util.concurrent.CompletableFuture;
-// Configures using the `HANZO_API_KEY` environment variable
+// Configures using the `hanzo.apiKey` and `hanzo.baseUrl` system properties
+// Or configures using the `HANZO_API_KEY` and `HANZO_BASE_URL` environment variables
HanzoClient client = HanzoOkHttpClient.fromEnv();
CompletableFuture response = client.async().getHome();
@@ -145,7 +176,8 @@ import ai.hanzo.api.models.ClientGetHomeParams;
import ai.hanzo.api.models.ClientGetHomeResponse;
import java.util.concurrent.CompletableFuture;
-// Configures using the `HANZO_API_KEY` environment variable
+// Configures using the `hanzo.apiKey` and `hanzo.baseUrl` system properties
+// Or configures using the `HANZO_API_KEY` and `HANZO_BASE_URL` environment variables
HanzoClientAsync client = HanzoOkHttpClientAsync.fromEnv();
CompletableFuture response = client.getHome();
@@ -258,6 +290,8 @@ The SDK throws custom unchecked exception types:
- [`HanzoIoException`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/errors/HanzoIoException.kt): I/O networking errors.
+- [`HanzoRetryableException`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/errors/HanzoRetryableException.kt): Generic error indicating a failure that could be retried by the client.
+
- [`HanzoInvalidDataException`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/errors/HanzoInvalidDataException.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.
- [`HanzoException`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/errors/HanzoException.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.
@@ -269,20 +303,39 @@ The SDK uses the standard [OkHttp logging interceptor](https://github.com/square
Enable logging by setting the `HANZO_LOG` environment variable to `info`:
```sh
-$ export HANZO_LOG=info
+export HANZO_LOG=info
```
Or to `debug` for more verbose logging:
```sh
-$ export HANZO_LOG=debug
+export HANZO_LOG=debug
```
+## ProGuard and R8
+
+Although the SDK uses reflection, it is still usable with [ProGuard](https://github.com/Guardsquare/proguard) and [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) because `hanzo-java-core` is published with a [configuration file](hanzo-java-core/src/main/resources/META-INF/proguard/hanzo-java-core.pro) containing [keep rules](https://www.guardsquare.com/manual/configuration/usage).
+
+ProGuard and R8 should automatically detect and use the published rules, but you can also manually copy the keep rules if necessary.
+
+## Jackson
+
+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.
+
+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).
+
+If the SDK threw an exception, but you're _certain_ the version is compatible, then disable the version check using the `checkJacksonVersionCompatibility` on [`HanzoOkHttpClient`](hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClient.kt) or [`HanzoOkHttpClientAsync`](hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClientAsync.kt).
+
+> [!CAUTION]
+> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
+
+Also note that there are bugs in older Jackson versions that can affect the SDK. We don't work around all Jackson bugs ([example](https://github.com/FasterXML/jackson-databind/issues/3240)) and expect users to upgrade Jackson for those instead.
+
## Network options
### Retries
-The SDK automatically retries 2 times by default, with a short exponential backoff.
+The SDK automatically retries 2 times by default, with a short exponential backoff between requests.
Only the following error types are retried:
@@ -292,7 +345,7 @@ Only the following error types are retried:
- 429 Rate Limit
- 5xx Internal
-The API may also explicitly instruct the SDK to retry or not retry a response.
+The API may also explicitly instruct the SDK to retry or not retry a request.
To set a custom number of retries, configure the client using the `maxRetries` method:
@@ -313,7 +366,6 @@ Requests time out after 1 minute by default.
To set a custom timeout, configure the method call using the `timeout` method:
```java
-import ai.hanzo.api.models.ClientGetHomeParams;
import ai.hanzo.api.models.ClientGetHomeResponse;
ClientGetHomeResponse response = client.getHome(RequestOptions.builder().timeout(Duration.ofSeconds(30)).build());
@@ -352,6 +404,27 @@ HanzoClient client = HanzoOkHttpClient.builder()
.build();
```
+### HTTPS
+
+> [!NOTE]
+> Most applications should not call these methods, and instead use the system defaults. The defaults include
+> special optimizations that can be lost if the implementations are modified.
+
+To configure how HTTPS connections are secured, configure the client using the `sslSocketFactory`, `trustManager`, and `hostnameVerifier` methods:
+
+```java
+import ai.hanzo.api.client.HanzoClient;
+import ai.hanzo.api.client.okhttp.HanzoOkHttpClient;
+
+HanzoClient client = HanzoOkHttpClient.builder()
+ .fromEnv()
+ // If `sslSocketFactory` is set, then `trustManager` must be set, and vice versa.
+ .sslSocketFactory(yourSSLSocketFactory)
+ .trustManager(yourTrustManager)
+ .hostnameVerifier(yourHostnameVerifier)
+ .build();
+```
+
### Environments
The SDK sends requests to the production by default. To send requests to a different environment, configure the client like so:
@@ -366,6 +439,42 @@ HanzoClient client = HanzoOkHttpClient.builder()
.build();
```
+### Custom HTTP client
+
+The SDK consists of three artifacts:
+
+- `hanzo-java-core`
+ - Contains core SDK logic
+ - Does not depend on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`HanzoClient`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClient.kt), [`HanzoClientAsync`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientAsync.kt), [`HanzoClientImpl`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientImpl.kt), and [`HanzoClientAsyncImpl`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientAsyncImpl.kt), all of which can work with any HTTP client
+- `hanzo-java-client-okhttp`
+ - Depends on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`HanzoOkHttpClient`](hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClient.kt) and [`HanzoOkHttpClientAsync`](hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClientAsync.kt), which provide a way to construct [`HanzoClientImpl`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientImpl.kt) and [`HanzoClientAsyncImpl`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientAsyncImpl.kt), respectively, using OkHttp
+- `hanzo-java`
+ - Depends on and exposes the APIs of both `hanzo-java-core` and `hanzo-java-client-okhttp`
+ - Does not have its own logic
+
+This structure allows replacing the SDK's default HTTP client without pulling in unnecessary dependencies.
+
+#### 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 [`hanzo-java` dependency](#installation) with `hanzo-java-core`
+2. Copy `hanzo-java-client-okhttp`'s [`OkHttpClient`](hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/OkHttpClient.kt) class into your code and customize it
+3. Construct [`HanzoClientImpl`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientImpl.kt) or [`HanzoClientAsyncImpl`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientAsyncImpl.kt), similarly to [`HanzoOkHttpClient`](hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClient.kt) or [`HanzoOkHttpClientAsync`](hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClientAsync.kt), using your customized client
+
+### Completely custom HTTP client
+
+To use a completely custom HTTP client:
+
+1. Replace your [`hanzo-java` dependency](#installation) with `hanzo-java-core`
+2. Write a class that implements the [`HttpClient`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/http/HttpClient.kt) interface
+3. Construct [`HanzoClientImpl`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientImpl.kt) or [`HanzoClientAsyncImpl`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientAsyncImpl.kt), similarly to [`HanzoOkHttpClient`](hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClient.kt) or [`HanzoOkHttpClientAsync`](hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClientAsync.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.
@@ -394,7 +503,7 @@ import ai.hanzo.api.core.JsonValue;
import ai.hanzo.api.models.model.ModelCreateParams;
ModelCreateParams params = ModelCreateParams.builder()
- .llmParams(ModelCreateParams.LlmParams.builder()
+ .litellmParams(ModelCreateParams.LitellmParams.builder()
.putAdditionalProperty("secretProperty", JsonValue.from("42"))
.build())
.build();
@@ -449,6 +558,20 @@ JsonValue complexValue = JsonValue.from(Map.of(
));
```
+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`](hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Values.kt):
+
+```java
+import ai.hanzo.api.core.JsonMissing;
+import ai.hanzo.api.models.ClientGetHomeParams;
+import ai.hanzo.api.models.openai.OpenAICreateParams;
+
+ClientGetHomeParams params = OpenAICreateParams.builder()
+ .endpoint(JsonMissing.of())
+ .build();
+```
+
### Response properties
To access undocumented response properties, call the `_additionalProperties()` method:
@@ -520,7 +643,6 @@ UtilTokenCounterResponse response = client.utils().tokenCounter(params).validate
Or configure the method call to validate the response using the `responseValidation` method:
```java
-import ai.hanzo.api.models.ClientGetHomeParams;
import ai.hanzo.api.models.ClientGetHomeResponse;
ClientGetHomeResponse response = client.getHome(RequestOptions.builder().responseValidation(true).build());
diff --git a/SECURITY.md b/SECURITY.md
index bada7faa..0b579e02 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -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 Hanzo please follow the respective company's security reporting guidelines.
+or products provided by Hanzo, please follow the respective company's security reporting guidelines.
### Hanzo Terms and Policies
-Please contact dev@hanzo.ai for any questions or concerns regarding security of our services.
+Please contact dev@hanzo.ai for any questions or concerns regarding the security of our services.
---
diff --git a/bin/check-release-environment b/bin/check-release-environment
index 8f014071..3a6a7b4a 100644
--- a/bin/check-release-environment
+++ b/bin/check-release-environment
@@ -3,19 +3,19 @@
errors=()
if [ -z "${SONATYPE_USERNAME}" ]; then
- errors+=("The HANZO_SONATYPE_USERNAME secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+ errors+=("The SONATYPE_USERNAME secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi
if [ -z "${SONATYPE_PASSWORD}" ]; then
- errors+=("The HANZO_SONATYPE_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+ errors+=("The SONATYPE_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi
if [ -z "${GPG_SIGNING_KEY}" ]; then
- errors+=("The HANZO_SONATYPE_GPG_SIGNING_KEY secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+ errors+=("The GPG_SIGNING_KEY secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi
if [ -z "${GPG_SIGNING_PASSWORD}" ]; then
- errors+=("The HANZO_SONATYPE_GPG_SIGNING_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+ errors+=("The GPG_SIGNING_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi
lenErrors=${#errors[@]}
diff --git a/build.gradle.kts b/build.gradle.kts
index 65121943..83d42120 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,7 +8,20 @@ repositories {
allprojects {
group = "ai.hanzo.api"
- version = "0.1.0-alpha.2" // x-release-please-version
+ version = "0.1.0-alpha.3" // x-release-please-version
+}
+
+subprojects {
+ // These are populated with dependencies by `buildSrc` scripts.
+ tasks.register("format") {
+ group = "Verification"
+ description = "Formats all source files."
+ }
+ tasks.register("lint") {
+ group = "Verification"
+ description = "Verifies all source files are formatted."
+ }
+ apply(plugin = "org.jetbrains.dokka")
}
subprojects {
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 778c89de..c6dc92ec 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -10,7 +10,6 @@ repositories {
}
dependencies {
- 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/hanzo.java.gradle.kts b/buildSrc/src/main/kotlin/hanzo.java.gradle.kts
index e39d9ac6..70fc33f4 100644
--- a/buildSrc/src/main/kotlin/hanzo.java.gradle.kts
+++ b/buildSrc/src/main/kotlin/hanzo.java.gradle.kts
@@ -1,27 +1,16 @@
-import com.diffplug.gradle.spotless.SpotlessExtension
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
plugins {
`java-library`
- id("com.diffplug.spotless")
}
repositories {
mavenCentral()
}
-configure {
- java {
- importOrder()
- removeUnusedImports()
- palantirJavaFormat()
- toggleOffOn()
- }
-}
-
java {
toolchain {
- languageVersion.set(JavaLanguageVersion.of(17))
+ languageVersion.set(JavaLanguageVersion.of(21))
}
sourceCompatibility = JavaVersion.VERSION_1_8
@@ -53,3 +42,86 @@ tasks.withType().configureEach {
exceptionFormat = TestExceptionFormat.FULL
}
}
+
+val palantir by configurations.creating
+dependencies {
+ palantir("com.palantir.javaformat:palantir-java-format:2.73.0")
+}
+
+fun registerPalantir(
+ name: String,
+ description: String,
+) {
+ val javaName = "${name}Java"
+ tasks.register(javaName) {
+ group = "Verification"
+ this.description = description
+
+ classpath = palantir
+ mainClass = "com.palantir.javaformat.java.Main"
+
+ // Avoid an `IllegalAccessError` on Java 9+.
+ jvmArgs(
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
+ )
+
+ // Use paths relative to the current module.
+ val argumentFile =
+ project.layout.buildDirectory.file("palantir-$name-args.txt").get().asFile
+ val lastRunTimeFile =
+ project.layout.buildDirectory.file("palantir-$name-last-run.txt").get().asFile
+
+ // Read the time when this task was last executed for this module (if ever).
+ val lastRunTime = lastRunTimeFile.takeIf { it.exists() }?.readText()?.toLongOrNull() ?: 0L
+
+ // Use a `fileTree` relative to the module's source directory.
+ val javaFiles = project.fileTree("src") { include("**/*.java") }
+
+ // Determine if any files need to be formatted or linted and continue only if there is at least
+ // one file.
+ onlyIf { javaFiles.any { it.lastModified() > lastRunTime } }
+
+ inputs.files(javaFiles)
+
+ doFirst {
+ // Create the argument file and set the preferred formatting style.
+ argumentFile.parentFile.mkdirs()
+ argumentFile.writeText("--palantir\n")
+
+ if (name == "lint") {
+ // For lint, do a dry run, so no files are modified. Set the exit code to 1 (instead of
+ // the default 0) if any files need to be formatted, indicating that linting has failed.
+ argumentFile.appendText("--dry-run\n")
+ argumentFile.appendText("--set-exit-if-changed\n")
+ } else {
+ // `--dry-run` and `--replace` (for in-place formatting) are mutually exclusive.
+ argumentFile.appendText("--replace\n")
+ }
+
+ // Write the modified files to the argument file.
+ javaFiles.filter { it.lastModified() > lastRunTime }
+ .forEach { argumentFile.appendText("${it.absolutePath}\n") }
+ }
+
+ doLast {
+ // Record the last execution time for later up-to-date checking.
+ lastRunTimeFile.writeText(System.currentTimeMillis().toString())
+ }
+
+ // Pass the argument file using the @ symbol
+ args = listOf("@${argumentFile.absolutePath}")
+
+ outputs.upToDateWhen { javaFiles.none { it.lastModified() > lastRunTime } }
+ }
+
+ tasks.named(name) {
+ dependsOn(tasks.named(javaName))
+ }
+}
+
+registerPalantir(name = "format", description = "Formats all Java source files.")
+registerPalantir(name = "lint", description = "Verifies all Java source files are formatted.")
diff --git a/buildSrc/src/main/kotlin/hanzo.kotlin.gradle.kts b/buildSrc/src/main/kotlin/hanzo.kotlin.gradle.kts
index 3f7ee106..fc8f35d8 100644
--- a/buildSrc/src/main/kotlin/hanzo.kotlin.gradle.kts
+++ b/buildSrc/src/main/kotlin/hanzo.kotlin.gradle.kts
@@ -1,4 +1,3 @@
-import com.diffplug.gradle.spotless.SpotlessExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
@@ -7,9 +6,13 @@ plugins {
kotlin("jvm")
}
+repositories {
+ mavenCentral()
+}
+
kotlin {
jvmToolchain {
- languageVersion.set(JavaLanguageVersion.of(17))
+ languageVersion.set(JavaLanguageVersion.of(21))
}
compilerOptions {
@@ -27,15 +30,80 @@ kotlin {
}
}
-configure {
- kotlin {
- ktfmt().kotlinlangStyle()
- toggleOffOn()
- }
+tasks.withType().configureEach {
+ systemProperty("junit.jupiter.execution.parallel.enabled", true)
+ systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
+
+ // `SKIP_MOCK_TESTS` affects which tests run so it must be added as input for proper cache invalidation.
+ inputs.property("skipMockTests", System.getenv("SKIP_MOCK_TESTS")).optional(true)
}
-// Run tests in parallel to some degree.
-tasks.withType().configureEach {
- maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
- forkEvery = 100
+val ktfmt by configurations.creating
+dependencies {
+ ktfmt("com.facebook:ktfmt:0.56")
+}
+
+fun registerKtfmt(
+ name: String,
+ description: String,
+) {
+ val kotlinName = "${name}Kotlin"
+ tasks.register(kotlinName) {
+ group = "Verification"
+ this.description = description
+
+ classpath = ktfmt
+ mainClass = "com.facebook.ktfmt.cli.Main"
+
+ // Use paths relative to the current module.
+ val argumentFile = project.layout.buildDirectory.file("ktfmt-$name-args.txt").get().asFile
+ val lastRunTimeFile =
+ project.layout.buildDirectory.file("ktfmt-$name-last-run.txt").get().asFile
+
+ // Read the time when this task was last executed for this module (if ever).
+ val lastRunTime = lastRunTimeFile.takeIf { it.exists() }?.readText()?.toLongOrNull() ?: 0L
+
+ // Use a `fileTree` relative to the module's source directory.
+ val kotlinFiles = project.fileTree("src") { include("**/*.kt") }
+
+ // Determine if any files need to be formatted or linted and continue only if there is at least
+ // one file (otherwise Ktfmt will fail).
+ onlyIf { kotlinFiles.any { it.lastModified() > lastRunTime } }
+
+ inputs.files(kotlinFiles)
+
+ doFirst {
+ // Create the argument file and set the preferred formatting style.
+ argumentFile.parentFile.mkdirs()
+ argumentFile.writeText("--kotlinlang-style\n")
+
+ if (name == "lint") {
+ // For lint, do a dry run, so no files are modified. Set the exit code to 1 (instead of
+ // the default 0) if any files need to be formatted, indicating that linting has failed.
+ argumentFile.appendText("--dry-run\n")
+ argumentFile.appendText("--set-exit-if-changed\n")
+ }
+
+ // Write the modified files to the argument file.
+ kotlinFiles.filter { it.lastModified() > lastRunTime }
+ .forEach { argumentFile.appendText("${it.absolutePath}\n") }
+ }
+
+ doLast {
+ // Record the last execution time for later up-to-date checking.
+ lastRunTimeFile.writeText(System.currentTimeMillis().toString())
+ }
+
+ // Pass the argument file using the @ symbol
+ args = listOf("@${argumentFile.absolutePath}")
+
+ outputs.upToDateWhen { kotlinFiles.none { it.lastModified() > lastRunTime } }
+ }
+
+ tasks.named(name) {
+ dependsOn(tasks.named(kotlinName))
+ }
}
+
+registerKtfmt(name = "format", description = "Formats all Kotlin source files.")
+registerKtfmt(name = "lint", description = "Verifies all Kotlin source files are formatted.")
diff --git a/buildSrc/src/main/kotlin/hanzo.publish.gradle.kts b/buildSrc/src/main/kotlin/hanzo.publish.gradle.kts
index 9e539251..2e3484a9 100644
--- a/buildSrc/src/main/kotlin/hanzo.publish.gradle.kts
+++ b/buildSrc/src/main/kotlin/hanzo.publish.gradle.kts
@@ -7,6 +7,17 @@ plugins {
id("com.vanniktech.maven.publish")
}
+publishing {
+ repositories {
+ if (project.hasProperty("publishLocal")) {
+ maven {
+ name = "LocalFileSystem"
+ url = uri("${rootProject.layout.buildDirectory.get()}/local-maven-repo")
+ }
+ }
+ }
+}
+
repositories {
gradlePluginPortal()
mavenCentral()
@@ -17,8 +28,10 @@ extra["signingInMemoryKeyId"] = System.getenv("GPG_SIGNING_KEY_ID")
extra["signingInMemoryKeyPassword"] = System.getenv("GPG_SIGNING_PASSWORD")
configure {
- signAllPublications()
- publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ if (!project.hasProperty("publishLocal")) {
+ signAllPublications()
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
+ }
coordinates(project.group.toString(), project.name, project.version.toString())
configure(
@@ -29,8 +42,8 @@ configure {
)
pom {
- name.set("Hanzo API")
- description.set("Call 100+ LLMs in the OpenAI format. [**Docs**](https://docs.hanzo.ai/)\n\nš [`Hanzo Console`](https://cloud.hanzo.ai). Create, Edit API Keys.\n\nšø [`LLM Models`](https://models.hanzo.ai/).")
+ name.set("LiteLLM API")
+ description.set("Proxy Server to call 100+ LLMs in the OpenAI format.\n[**Customize Swagger Docs**](https://docs.litellm.ai/docs/proxy/enterprise#swagger-docs---custom-routes--branding)\n\nš [`LiteLLM Admin Panel on /ui`](/ui). Create, Edit Keys with SSO. Having\nissues? Try [`Fallback Login`](/fallback/login)\n\nšø [`LiteLLM Model Cost Map`](https://models.litellm.ai/).\n\nš [`LiteLLM Model Hub`](/ui/model_hub_table). See available models on the\nproxy. [**Docs**](https://docs.litellm.ai/docs/proxy/ai_hub)")
url.set("https://docs.hanzo.ai")
licenses {
@@ -53,3 +66,7 @@ configure {
}
}
}
+
+tasks.withType().configureEach {
+ isZip64 = true
+}
diff --git a/gradle.properties b/gradle.properties
index 0c8d4ded..6680f9ce 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,18 @@
org.gradle.caching=true
+org.gradle.configuration-cache=true
org.gradle.parallel=true
org.gradle.daemon=false
-org.gradle.jvmargs=-Xmx4g
-kotlin.daemon.jvmargs=-Xmx4g
+# These options improve our compilation and test performance. They are inherited by the Kotlin daemon.
+org.gradle.jvmargs=\
+ -Xms2g \
+ -Xmx8g \
+ -XX:+UseParallelGC \
+ -XX:InitialCodeCacheSize=256m \
+ -XX:ReservedCodeCacheSize=1G \
+ -XX:MetaspaceSize=512m \
+ -XX:MaxMetaspaceSize=2G \
+ -XX:TieredStopAtLevel=1 \
+ -XX:GCTimeRatio=4 \
+ -XX:CICompilerCount=4 \
+ -XX:+OptimizeStringConcat \
+ -XX:+UseStringDeduplication
diff --git a/hanzo-java-client-okhttp/build.gradle.kts b/hanzo-java-client-okhttp/build.gradle.kts
index 20cd525a..4307d79f 100644
--- a/hanzo-java-client-okhttp/build.gradle.kts
+++ b/hanzo-java-client-okhttp/build.gradle.kts
@@ -11,4 +11,5 @@ dependencies {
testImplementation(kotlin("test"))
testImplementation("org.assertj:assertj-core:3.25.3")
+ testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2")
}
diff --git a/hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClient.kt b/hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClient.kt
index 1965d611..139f16c1 100644
--- a/hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClient.kt
+++ b/hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClient.kt
@@ -5,21 +5,39 @@ package ai.hanzo.api.client.okhttp
import ai.hanzo.api.client.HanzoClient
import ai.hanzo.api.client.HanzoClientImpl
import ai.hanzo.api.core.ClientOptions
+import ai.hanzo.api.core.Sleeper
import ai.hanzo.api.core.Timeout
import ai.hanzo.api.core.http.Headers
+import ai.hanzo.api.core.http.HttpClient
import ai.hanzo.api.core.http.QueryParams
+import ai.hanzo.api.core.jsonMapper
import com.fasterxml.jackson.databind.json.JsonMapper
import java.net.Proxy
import java.time.Clock
import java.time.Duration
-
+import java.util.Optional
+import java.util.concurrent.ExecutorService
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509TrustManager
+import kotlin.jvm.optionals.getOrNull
+
+/**
+ * A class that allows building an instance of [HanzoClient] with [OkHttpClient] as the underlying
+ * [HttpClient].
+ */
class HanzoOkHttpClient private constructor() {
companion object {
- /** Returns a mutable builder for constructing an instance of [HanzoOkHttpClient]. */
+ /** Returns a mutable builder for constructing an instance of [HanzoClient]. */
@JvmStatic fun builder() = Builder()
+ /**
+ * Returns a client configured using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
@JvmStatic fun fromEnv(): HanzoClient = builder().fromEnv().build()
}
@@ -27,17 +45,84 @@ class HanzoOkHttpClient private constructor() {
class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
- private var baseUrl: String = ClientOptions.PRODUCTION_URL
- private var timeout: Timeout = Timeout.default()
+ private var dispatcherExecutorService: ExecutorService? = null
private var proxy: Proxy? = null
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
+
+ /**
+ * The executor service to use for running HTTP requests.
+ *
+ * Defaults to OkHttp's
+ * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104).
+ *
+ * This class takes ownership of the executor service and shuts it down when closed.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
+ this.dispatcherExecutorService = dispatcherExecutorService
+ }
+
+ /**
+ * Alias for calling [Builder.dispatcherExecutorService] with
+ * `dispatcherExecutorService.orElse(null)`.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: Optional) =
+ dispatcherExecutorService(dispatcherExecutorService.getOrNull())
+
+ fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+
+ /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
+ fun proxy(proxy: Optional) = proxy(proxy.getOrNull())
+
+ /**
+ * The socket factory used to secure HTTPS connections.
+ *
+ * If this is set, then [trustManager] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
+ this.sslSocketFactory = sslSocketFactory
+ }
+
+ /** Alias for calling [Builder.sslSocketFactory] with `sslSocketFactory.orElse(null)`. */
+ fun sslSocketFactory(sslSocketFactory: Optional) =
+ sslSocketFactory(sslSocketFactory.getOrNull())
+
+ /**
+ * The trust manager used to secure HTTPS connections.
+ *
+ * If this is set, then [sslSocketFactory] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun trustManager(trustManager: X509TrustManager?) = apply {
+ this.trustManager = trustManager
+ }
- fun sandbox() = apply { baseUrl(ClientOptions.SANDBOX_URL) }
+ /** Alias for calling [Builder.trustManager] with `trustManager.orElse(null)`. */
+ fun trustManager(trustManager: Optional) =
+ trustManager(trustManager.getOrNull())
- fun baseUrl(baseUrl: String) = apply {
- clientOptions.baseUrl(baseUrl)
- this.baseUrl = baseUrl
+ /**
+ * The verifier used to confirm that response certificates apply to requested hostnames for
+ * HTTPS connections.
+ *
+ * If unset, then a default hostname verifier is used.
+ */
+ fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply {
+ this.hostnameVerifier = hostnameVerifier
}
+ /** Alias for calling [Builder.hostnameVerifier] with `hostnameVerifier.orElse(null)`. */
+ fun hostnameVerifier(hostnameVerifier: Optional) =
+ hostnameVerifier(hostnameVerifier.getOrNull())
+
/**
* 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).
@@ -49,10 +134,97 @@ class HanzoOkHttpClient private constructor() {
clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [ai.hanzo.api.core.jsonMapper]. The default is usually sufficient and rarely
+ * needs to be overridden.
+ */
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
+ /**
+ * The interface to use for delaying execution, like during retries.
+ *
+ * This is primarily useful for using fake delays in tests.
+ *
+ * Defaults to real execution delays.
+ *
+ * This class takes ownership of the sleeper and closes it when closed.
+ */
+ fun sleeper(sleeper: Sleeper) = apply { clientOptions.sleeper(sleeper) }
+
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.hanzo.ai`.
+ *
+ * The following other environments, with dedicated builder methods, are available:
+ * - sandbox: `https://api.sandbox.hanzo.ai`
+ */
+ fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */
+ fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull())
+
+ /** Sets [baseUrl] to `https://api.sandbox.hanzo.ai`. */
+ fun sandbox() = apply { clientOptions.sandbox() }
+
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
+ fun responseValidation(responseValidation: Boolean) = apply {
+ clientOptions.responseValidation(responseValidation)
+ }
+
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
+ fun timeout(timeout: Timeout) = apply { clientOptions.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) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * 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 request.
+ *
+ * Defaults to 2.
+ */
+ fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
+
+ /** The default name of the subscription key header of Azure */
+ fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
+
fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
fun headers(headers: Map>) = apply {
@@ -133,30 +305,11 @@ class HanzoOkHttpClient private constructor() {
clientOptions.removeAllQueryParams(keys)
}
- 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.
+ * Updates configuration using system properties and environment variables.
*
- * For fine-grained control, pass a [Timeout] object.
+ * @see ClientOptions.Builder.fromEnv
*/
- fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
-
- fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
-
- fun proxy(proxy: Proxy) = apply { this.proxy = proxy }
-
- fun responseValidation(responseValidation: Boolean) = apply {
- clientOptions.responseValidation(responseValidation)
- }
-
- fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
-
fun fromEnv() = apply { clientOptions.fromEnv() }
/**
@@ -169,9 +322,12 @@ class HanzoOkHttpClient private constructor() {
clientOptions
.httpClient(
OkHttpClient.builder()
- .baseUrl(baseUrl)
- .timeout(timeout)
+ .timeout(clientOptions.timeout())
.proxy(proxy)
+ .dispatcherExecutorService(dispatcherExecutorService)
+ .sslSocketFactory(sslSocketFactory)
+ .trustManager(trustManager)
+ .hostnameVerifier(hostnameVerifier)
.build()
)
.build()
diff --git a/hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClientAsync.kt b/hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClientAsync.kt
index 68a77b16..d6689bbc 100644
--- a/hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClientAsync.kt
+++ b/hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/HanzoOkHttpClientAsync.kt
@@ -5,21 +5,39 @@ package ai.hanzo.api.client.okhttp
import ai.hanzo.api.client.HanzoClientAsync
import ai.hanzo.api.client.HanzoClientAsyncImpl
import ai.hanzo.api.core.ClientOptions
+import ai.hanzo.api.core.Sleeper
import ai.hanzo.api.core.Timeout
import ai.hanzo.api.core.http.Headers
+import ai.hanzo.api.core.http.HttpClient
import ai.hanzo.api.core.http.QueryParams
+import ai.hanzo.api.core.jsonMapper
import com.fasterxml.jackson.databind.json.JsonMapper
import java.net.Proxy
import java.time.Clock
import java.time.Duration
-
+import java.util.Optional
+import java.util.concurrent.ExecutorService
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509TrustManager
+import kotlin.jvm.optionals.getOrNull
+
+/**
+ * A class that allows building an instance of [HanzoClientAsync] with [OkHttpClient] as the
+ * underlying [HttpClient].
+ */
class HanzoOkHttpClientAsync private constructor() {
companion object {
- /** Returns a mutable builder for constructing an instance of [HanzoOkHttpClientAsync]. */
+ /** Returns a mutable builder for constructing an instance of [HanzoClientAsync]. */
@JvmStatic fun builder() = Builder()
+ /**
+ * Returns a client configured using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
@JvmStatic fun fromEnv(): HanzoClientAsync = builder().fromEnv().build()
}
@@ -27,17 +45,84 @@ class HanzoOkHttpClientAsync private constructor() {
class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
- private var baseUrl: String = ClientOptions.PRODUCTION_URL
- private var timeout: Timeout = Timeout.default()
+ private var dispatcherExecutorService: ExecutorService? = null
private var proxy: Proxy? = null
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
+
+ /**
+ * The executor service to use for running HTTP requests.
+ *
+ * Defaults to OkHttp's
+ * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104).
+ *
+ * This class takes ownership of the executor service and shuts it down when closed.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
+ this.dispatcherExecutorService = dispatcherExecutorService
+ }
+
+ /**
+ * Alias for calling [Builder.dispatcherExecutorService] with
+ * `dispatcherExecutorService.orElse(null)`.
+ */
+ fun dispatcherExecutorService(dispatcherExecutorService: Optional) =
+ dispatcherExecutorService(dispatcherExecutorService.getOrNull())
+
+ fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+
+ /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
+ fun proxy(proxy: Optional) = proxy(proxy.getOrNull())
+
+ /**
+ * The socket factory used to secure HTTPS connections.
+ *
+ * If this is set, then [trustManager] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
+ this.sslSocketFactory = sslSocketFactory
+ }
+
+ /** Alias for calling [Builder.sslSocketFactory] with `sslSocketFactory.orElse(null)`. */
+ fun sslSocketFactory(sslSocketFactory: Optional) =
+ sslSocketFactory(sslSocketFactory.getOrNull())
+
+ /**
+ * The trust manager used to secure HTTPS connections.
+ *
+ * If this is set, then [sslSocketFactory] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun trustManager(trustManager: X509TrustManager?) = apply {
+ this.trustManager = trustManager
+ }
- fun sandbox() = apply { baseUrl(ClientOptions.SANDBOX_URL) }
+ /** Alias for calling [Builder.trustManager] with `trustManager.orElse(null)`. */
+ fun trustManager(trustManager: Optional) =
+ trustManager(trustManager.getOrNull())
- fun baseUrl(baseUrl: String) = apply {
- clientOptions.baseUrl(baseUrl)
- this.baseUrl = baseUrl
+ /**
+ * The verifier used to confirm that response certificates apply to requested hostnames for
+ * HTTPS connections.
+ *
+ * If unset, then a default hostname verifier is used.
+ */
+ fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply {
+ this.hostnameVerifier = hostnameVerifier
}
+ /** Alias for calling [Builder.hostnameVerifier] with `hostnameVerifier.orElse(null)`. */
+ fun hostnameVerifier(hostnameVerifier: Optional) =
+ hostnameVerifier(hostnameVerifier.getOrNull())
+
/**
* 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).
@@ -49,10 +134,97 @@ class HanzoOkHttpClientAsync private constructor() {
clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [ai.hanzo.api.core.jsonMapper]. The default is usually sufficient and rarely
+ * needs to be overridden.
+ */
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
+ /**
+ * The interface to use for delaying execution, like during retries.
+ *
+ * This is primarily useful for using fake delays in tests.
+ *
+ * Defaults to real execution delays.
+ *
+ * This class takes ownership of the sleeper and closes it when closed.
+ */
+ fun sleeper(sleeper: Sleeper) = apply { clientOptions.sleeper(sleeper) }
+
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.hanzo.ai`.
+ *
+ * The following other environments, with dedicated builder methods, are available:
+ * - sandbox: `https://api.sandbox.hanzo.ai`
+ */
+ fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */
+ fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull())
+
+ /** Sets [baseUrl] to `https://api.sandbox.hanzo.ai`. */
+ fun sandbox() = apply { clientOptions.sandbox() }
+
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
+ fun responseValidation(responseValidation: Boolean) = apply {
+ clientOptions.responseValidation(responseValidation)
+ }
+
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
+ fun timeout(timeout: Timeout) = apply { clientOptions.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) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * 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 request.
+ *
+ * Defaults to 2.
+ */
+ fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
+
+ /** The default name of the subscription key header of Azure */
+ fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
+
fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
fun headers(headers: Map>) = apply {
@@ -133,30 +305,11 @@ class HanzoOkHttpClientAsync private constructor() {
clientOptions.removeAllQueryParams(keys)
}
- 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.
+ * Updates configuration using system properties and environment variables.
*
- * For fine-grained control, pass a [Timeout] object.
+ * @see ClientOptions.Builder.fromEnv
*/
- fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
-
- fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
-
- fun proxy(proxy: Proxy) = apply { this.proxy = proxy }
-
- fun responseValidation(responseValidation: Boolean) = apply {
- clientOptions.responseValidation(responseValidation)
- }
-
- fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
-
fun fromEnv() = apply { clientOptions.fromEnv() }
/**
@@ -169,9 +322,12 @@ class HanzoOkHttpClientAsync private constructor() {
clientOptions
.httpClient(
OkHttpClient.builder()
- .baseUrl(baseUrl)
- .timeout(timeout)
+ .timeout(clientOptions.timeout())
.proxy(proxy)
+ .dispatcherExecutorService(dispatcherExecutorService)
+ .sslSocketFactory(sslSocketFactory)
+ .trustManager(trustManager)
+ .hostnameVerifier(hostnameVerifier)
.build()
)
.build()
diff --git a/hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/OkHttpClient.kt b/hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/OkHttpClient.kt
index 3b93ec84..3dc8e1a6 100644
--- a/hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/OkHttpClient.kt
+++ b/hanzo-java-client-okhttp/src/main/kotlin/ai/hanzo/api/client/okhttp/OkHttpClient.kt
@@ -2,7 +2,6 @@ package ai.hanzo.api.client.okhttp
import ai.hanzo.api.core.RequestOptions
import ai.hanzo.api.core.Timeout
-import ai.hanzo.api.core.checkRequired
import ai.hanzo.api.core.http.Headers
import ai.hanzo.api.core.http.HttpClient
import ai.hanzo.api.core.http.HttpMethod
@@ -14,10 +13,15 @@ import java.io.IOException
import java.io.InputStream
import java.net.Proxy
import java.time.Duration
+import java.util.concurrent.CancellationException
import java.util.concurrent.CompletableFuture
+import java.util.concurrent.ExecutorService
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509TrustManager
import okhttp3.Call
import okhttp3.Callback
-import okhttp3.HttpUrl
+import okhttp3.Dispatcher
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
@@ -29,8 +33,7 @@ import okhttp3.logging.HttpLoggingInterceptor
import okio.BufferedSink
class OkHttpClient
-private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val baseUrl: HttpUrl) :
- HttpClient {
+private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient {
override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
val call = newCall(request, requestOptions)
@@ -50,20 +53,25 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
): CompletableFuture {
val future = CompletableFuture()
- request.body?.run { future.whenComplete { _, _ -> close() } }
-
- newCall(request, requestOptions)
- .enqueue(
- object : Callback {
- override fun onResponse(call: Call, response: Response) {
- future.complete(response.toResponse())
- }
+ val call = newCall(request, requestOptions)
+ call.enqueue(
+ object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ future.complete(response.toResponse())
+ }
- override fun onFailure(call: Call, e: IOException) {
- future.completeExceptionally(HanzoIoException("Request failed", e))
- }
+ override fun onFailure(call: Call, e: IOException) {
+ future.completeExceptionally(HanzoIoException("Request failed", e))
}
- )
+ }
+ )
+
+ future.whenComplete { _, e ->
+ if (e is CancellationException) {
+ call.cancel()
+ }
+ request.body?.close()
+ }
return future
}
@@ -86,7 +94,7 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
if (logLevel != null) {
clientBuilder.addNetworkInterceptor(
HttpLoggingInterceptor().setLevel(logLevel).apply {
- redactHeader("Ocp-Apim-Subscription-Key")
+ redactHeader("x-litellm-api-key")
}
)
}
@@ -111,19 +119,19 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
val builder = Request.Builder().url(toUrl()).method(method.name, body)
headers.names().forEach { name ->
- headers.values(name).forEach { builder.header(name, it) }
+ headers.values(name).forEach { builder.addHeader(name, it) }
}
if (
!headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
) {
- builder.header(
+ builder.addHeader(
"X-Stainless-Read-Timeout",
Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
)
}
if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
- builder.header(
+ builder.addHeader(
"X-Stainless-Timeout",
Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
)
@@ -142,11 +150,7 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
}
private fun HttpRequest.toUrl(): String {
- url?.let {
- return it
- }
-
- val builder = baseUrl.newBuilder()
+ val builder = baseUrl.toHttpUrl().newBuilder()
pathSegments.forEach(builder::addPathSegment)
queryParams.keys().forEach { key ->
queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
@@ -196,11 +200,12 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
class Builder internal constructor() {
- private var baseUrl: HttpUrl? = null
private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
-
- fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl.toHttpUrl() }
+ private var dispatcherExecutorService: ExecutorService? = null
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
@@ -208,16 +213,53 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+ fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply {
+ this.dispatcherExecutorService = dispatcherExecutorService
+ }
+
+ fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
+ this.sslSocketFactory = sslSocketFactory
+ }
+
+ fun trustManager(trustManager: X509TrustManager?) = apply {
+ this.trustManager = trustManager
+ }
+
+ fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply {
+ this.hostnameVerifier = hostnameVerifier
+ }
+
fun build(): OkHttpClient =
OkHttpClient(
okhttp3.OkHttpClient.Builder()
+ // `RetryingHttpClient` handles retries if the user enabled them.
+ .retryOnConnectionFailure(false)
.connectTimeout(timeout.connect())
.readTimeout(timeout.read())
.writeTimeout(timeout.write())
.callTimeout(timeout.request())
.proxy(proxy)
- .build(),
- checkRequired("baseUrl", baseUrl),
+ .apply {
+ dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) }
+
+ val sslSocketFactory = sslSocketFactory
+ val trustManager = trustManager
+ if (sslSocketFactory != null && trustManager != null) {
+ sslSocketFactory(sslSocketFactory, trustManager)
+ } else {
+ check((sslSocketFactory != null) == (trustManager != null)) {
+ "Both or none of `sslSocketFactory` and `trustManager` must be set, but only one was set"
+ }
+ }
+
+ hostnameVerifier?.let(::hostnameVerifier)
+ }
+ .build()
+ .apply {
+ // We usually make all our requests to the same host so it makes sense to
+ // raise the per-host limit to the overall limit.
+ dispatcher.maxRequestsPerHost = dispatcher.maxRequests
+ }
)
}
}
diff --git a/hanzo-java-client-okhttp/src/test/kotlin/ai/hanzo/api/client/okhttp/OkHttpClientTest.kt b/hanzo-java-client-okhttp/src/test/kotlin/ai/hanzo/api/client/okhttp/OkHttpClientTest.kt
new file mode 100644
index 00000000..7f855e39
--- /dev/null
+++ b/hanzo-java-client-okhttp/src/test/kotlin/ai/hanzo/api/client/okhttp/OkHttpClientTest.kt
@@ -0,0 +1,44 @@
+package ai.hanzo.api.client.okhttp
+
+import ai.hanzo.api.core.http.HttpMethod
+import ai.hanzo.api.core.http.HttpRequest
+import com.github.tomakehurst.wiremock.client.WireMock.*
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo
+import com.github.tomakehurst.wiremock.junit5.WireMockTest
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.parallel.ResourceLock
+
+@WireMockTest
+@ResourceLock("https://github.com/wiremock/wiremock/issues/169")
+internal class OkHttpClientTest {
+
+ private lateinit var baseUrl: String
+ private lateinit var httpClient: OkHttpClient
+
+ @BeforeEach
+ fun beforeEach(wmRuntimeInfo: WireMockRuntimeInfo) {
+ baseUrl = wmRuntimeInfo.httpBaseUrl
+ httpClient = OkHttpClient.builder().build()
+ }
+
+ @Test
+ fun executeAsync_whenFutureCancelled_cancelsUnderlyingCall() {
+ stubFor(post(urlPathEqualTo("/something")).willReturn(ok()))
+ val responseFuture =
+ httpClient.executeAsync(
+ HttpRequest.builder()
+ .method(HttpMethod.POST)
+ .baseUrl(baseUrl)
+ .addPathSegment("something")
+ .build()
+ )
+ val call = httpClient.okHttpClient.dispatcher.runningCalls().single()
+
+ responseFuture.cancel(false)
+
+ // Should have cancelled the underlying call
+ assertThat(call.isCanceled()).isTrue()
+ }
+}
diff --git a/hanzo-java-core/build.gradle.kts b/hanzo-java-core/build.gradle.kts
index 18a11075..4d1c6331 100644
--- a/hanzo-java-core/build.gradle.kts
+++ b/hanzo-java-core/build.gradle.kts
@@ -5,26 +5,28 @@ plugins {
configurations.all {
resolutionStrategy {
- // Compile and test against a lower Jackson version to ensure we're compatible with it.
- // We publish with a higher version (see below) to ensure users depend on a secure version by default.
- force("com.fasterxml.jackson.core:jackson-core:2.13.4")
- force("com.fasterxml.jackson.core:jackson-databind:2.13.4")
- force("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
- force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4")
- force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4")
- force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
+ // Compile and test against a lower Jackson version to ensure we're compatible with it. Note that
+ // we generally support 2.13.4, but test against 2.14.0 because 2.13.4 has some annoying (but
+ // niche) bugs (users should upgrade if they encounter them). We publish with a higher version
+ // (see below) to ensure users depend on a secure version by default.
+ force("com.fasterxml.jackson.core:jackson-core:2.14.0")
+ force("com.fasterxml.jackson.core:jackson-databind:2.14.0")
+ force("com.fasterxml.jackson.core:jackson-annotations:2.14.0")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.0")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.0")
+ force("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0")
}
}
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")
@@ -34,6 +36,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/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClient.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClient.kt
index 5ba61eca..8400c4d5 100644
--- a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClient.kt
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClient.kt
@@ -2,6 +2,7 @@
package ai.hanzo.api.client
+import ai.hanzo.api.core.ClientOptions
import ai.hanzo.api.core.RequestOptions
import ai.hanzo.api.core.http.HttpResponseFor
import ai.hanzo.api.models.ClientGetHomeParams
@@ -54,6 +55,7 @@ import ai.hanzo.api.services.blocking.UserService
import ai.hanzo.api.services.blocking.UtilService
import ai.hanzo.api.services.blocking.VertexAiService
import com.google.errorprone.annotations.MustBeClosed
+import java.util.function.Consumer
/**
* A client for interacting with the Hanzo REST API synchronously. You can also switch to
@@ -84,6 +86,13 @@ interface HanzoClient {
*/
fun withRawResponse(): WithRawResponse
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(modifier: Consumer): HanzoClient
+
fun models(): ModelService
fun openai(): OpenAIService
@@ -183,17 +192,17 @@ interface HanzoClient {
/** Home */
fun getHome(): ClientGetHomeResponse = getHome(ClientGetHomeParams.none())
- /** @see [getHome] */
+ /** @see getHome */
fun getHome(
params: ClientGetHomeParams = ClientGetHomeParams.none(),
requestOptions: RequestOptions = RequestOptions.none(),
): ClientGetHomeResponse
- /** @see [getHome] */
+ /** @see getHome */
fun getHome(params: ClientGetHomeParams = ClientGetHomeParams.none()): ClientGetHomeResponse =
getHome(params, RequestOptions.none())
- /** @see [getHome] */
+ /** @see getHome */
fun getHome(requestOptions: RequestOptions): ClientGetHomeResponse =
getHome(ClientGetHomeParams.none(), requestOptions)
@@ -213,6 +222,13 @@ interface HanzoClient {
/** A view of [HanzoClient] that provides access to raw HTTP responses for each method. */
interface WithRawResponse {
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(modifier: Consumer): HanzoClient.WithRawResponse
+
fun models(): ModelService.WithRawResponse
fun openai(): OpenAIService.WithRawResponse
@@ -316,20 +332,20 @@ interface HanzoClient {
@MustBeClosed
fun getHome(): HttpResponseFor = getHome(ClientGetHomeParams.none())
- /** @see [getHome] */
+ /** @see getHome */
@MustBeClosed
fun getHome(
params: ClientGetHomeParams = ClientGetHomeParams.none(),
requestOptions: RequestOptions = RequestOptions.none(),
): HttpResponseFor
- /** @see [getHome] */
+ /** @see getHome */
@MustBeClosed
fun getHome(
params: ClientGetHomeParams = ClientGetHomeParams.none()
): HttpResponseFor = getHome(params, RequestOptions.none())
- /** @see [getHome] */
+ /** @see getHome */
@MustBeClosed
fun getHome(requestOptions: RequestOptions): HttpResponseFor =
getHome(ClientGetHomeParams.none(), requestOptions)
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientAsync.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientAsync.kt
index e9905a45..7b73152f 100644
--- a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientAsync.kt
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientAsync.kt
@@ -2,6 +2,7 @@
package ai.hanzo.api.client
+import ai.hanzo.api.core.ClientOptions
import ai.hanzo.api.core.RequestOptions
import ai.hanzo.api.core.http.HttpResponseFor
import ai.hanzo.api.models.ClientGetHomeParams
@@ -53,8 +54,8 @@ import ai.hanzo.api.services.async.ThreadServiceAsync
import ai.hanzo.api.services.async.UserServiceAsync
import ai.hanzo.api.services.async.UtilServiceAsync
import ai.hanzo.api.services.async.VertexAiServiceAsync
-import com.google.errorprone.annotations.MustBeClosed
import java.util.concurrent.CompletableFuture
+import java.util.function.Consumer
/**
* A client for interacting with the Hanzo REST API asynchronously. You can also switch to
@@ -85,6 +86,13 @@ interface HanzoClientAsync {
*/
fun withRawResponse(): WithRawResponse
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(modifier: Consumer): HanzoClientAsync
+
fun models(): ModelServiceAsync
fun openai(): OpenAIServiceAsync
@@ -184,18 +192,18 @@ interface HanzoClientAsync {
/** Home */
fun getHome(): CompletableFuture = getHome(ClientGetHomeParams.none())
- /** @see [getHome] */
+ /** @see getHome */
fun getHome(
params: ClientGetHomeParams = ClientGetHomeParams.none(),
requestOptions: RequestOptions = RequestOptions.none(),
): CompletableFuture
- /** @see [getHome] */
+ /** @see getHome */
fun getHome(
params: ClientGetHomeParams = ClientGetHomeParams.none()
): CompletableFuture = getHome(params, RequestOptions.none())
- /** @see [getHome] */
+ /** @see getHome */
fun getHome(requestOptions: RequestOptions): CompletableFuture =
getHome(ClientGetHomeParams.none(), requestOptions)
@@ -215,6 +223,13 @@ interface HanzoClientAsync {
/** A view of [HanzoClientAsync] that provides access to raw HTTP responses for each method. */
interface WithRawResponse {
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(modifier: Consumer): HanzoClientAsync.WithRawResponse
+
fun models(): ModelServiceAsync.WithRawResponse
fun openai(): OpenAIServiceAsync.WithRawResponse
@@ -315,26 +330,22 @@ interface HanzoClientAsync {
* Returns a raw HTTP response for `get /`, but is otherwise the same as
* [HanzoClientAsync.getHome].
*/
- @MustBeClosed
fun getHome(): CompletableFuture> =
getHome(ClientGetHomeParams.none())
- /** @see [getHome] */
- @MustBeClosed
+ /** @see getHome */
fun getHome(
params: ClientGetHomeParams = ClientGetHomeParams.none(),
requestOptions: RequestOptions = RequestOptions.none(),
): CompletableFuture>
- /** @see [getHome] */
- @MustBeClosed
+ /** @see getHome */
fun getHome(
params: ClientGetHomeParams = ClientGetHomeParams.none()
): CompletableFuture> =
getHome(params, RequestOptions.none())
- /** @see [getHome] */
- @MustBeClosed
+ /** @see getHome */
fun getHome(
requestOptions: RequestOptions
): CompletableFuture> =
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientAsyncImpl.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientAsyncImpl.kt
index 81849c7f..1441a129 100644
--- a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientAsyncImpl.kt
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientAsyncImpl.kt
@@ -3,14 +3,14 @@
package ai.hanzo.api.client
import ai.hanzo.api.core.ClientOptions
-import ai.hanzo.api.core.JsonValue
import ai.hanzo.api.core.RequestOptions
import ai.hanzo.api.core.getPackageVersion
+import ai.hanzo.api.core.handlers.errorBodyHandler
import ai.hanzo.api.core.handlers.errorHandler
import ai.hanzo.api.core.handlers.jsonHandler
-import ai.hanzo.api.core.handlers.withErrorHandler
import ai.hanzo.api.core.http.HttpMethod
import ai.hanzo.api.core.http.HttpRequest
+import ai.hanzo.api.core.http.HttpResponse
import ai.hanzo.api.core.http.HttpResponse.Handler
import ai.hanzo.api.core.http.HttpResponseFor
import ai.hanzo.api.core.http.parseable
@@ -112,6 +112,7 @@ import ai.hanzo.api.services.async.UtilServiceAsyncImpl
import ai.hanzo.api.services.async.VertexAiServiceAsync
import ai.hanzo.api.services.async.VertexAiServiceAsyncImpl
import java.util.concurrent.CompletableFuture
+import java.util.function.Consumer
class HanzoClientAsyncImpl(private val clientOptions: ClientOptions) : HanzoClientAsync {
@@ -310,6 +311,9 @@ class HanzoClientAsyncImpl(private val clientOptions: ClientOptions) : HanzoClie
override fun withRawResponse(): HanzoClientAsync.WithRawResponse = withRawResponse
+ override fun withOptions(modifier: Consumer): HanzoClientAsync =
+ HanzoClientAsyncImpl(clientOptions.toBuilder().apply(modifier::accept).build())
+
override fun models(): ModelServiceAsync = models
override fun openai(): OpenAIServiceAsync = openai
@@ -413,12 +417,13 @@ class HanzoClientAsyncImpl(private val clientOptions: ClientOptions) : HanzoClie
// get /
withRawResponse().getHome(params, requestOptions).thenApply { it.parse() }
- override fun close() = clientOptions.httpClient.close()
+ override fun close() = clientOptions.close()
class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
HanzoClientAsync.WithRawResponse {
- private val errorHandler: Handler = errorHandler(clientOptions.jsonMapper)
+ private val errorHandler: Handler =
+ errorHandler(errorBodyHandler(clientOptions.jsonMapper))
private val models: ModelServiceAsync.WithRawResponse by lazy {
ModelServiceAsyncImpl.WithRawResponseImpl(clientOptions)
@@ -612,6 +617,13 @@ class HanzoClientAsyncImpl(private val clientOptions: ClientOptions) : HanzoClie
BudgetServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
+ override fun withOptions(
+ modifier: Consumer
+ ): HanzoClientAsync.WithRawResponse =
+ HanzoClientAsyncImpl.WithRawResponseImpl(
+ clientOptions.toBuilder().apply(modifier::accept).build()
+ )
+
override fun models(): ModelServiceAsync.WithRawResponse = models
override fun openai(): OpenAIServiceAsync.WithRawResponse = openai
@@ -710,7 +722,6 @@ class HanzoClientAsyncImpl(private val clientOptions: ClientOptions) : HanzoClie
private val getHomeHandler: Handler =
jsonHandler(clientOptions.jsonMapper)
- .withErrorHandler(errorHandler)
override fun getHome(
params: ClientGetHomeParams,
@@ -719,6 +730,7 @@ class HanzoClientAsyncImpl(private val clientOptions: ClientOptions) : HanzoClie
val request =
HttpRequest.builder()
.method(HttpMethod.GET)
+ .baseUrl(clientOptions.baseUrl())
.addPathSegments("")
.build()
.prepareAsync(clientOptions, params)
@@ -726,7 +738,7 @@ class HanzoClientAsyncImpl(private val clientOptions: ClientOptions) : HanzoClie
return request
.thenComposeAsync { clientOptions.httpClient.executeAsync(it, requestOptions) }
.thenApply { response ->
- response.parseable {
+ errorHandler.handle(response).parseable {
response
.use { getHomeHandler.handle(it) }
.also {
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientImpl.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientImpl.kt
index 866f48ca..43e98b9f 100644
--- a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientImpl.kt
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/client/HanzoClientImpl.kt
@@ -3,14 +3,14 @@
package ai.hanzo.api.client
import ai.hanzo.api.core.ClientOptions
-import ai.hanzo.api.core.JsonValue
import ai.hanzo.api.core.RequestOptions
import ai.hanzo.api.core.getPackageVersion
+import ai.hanzo.api.core.handlers.errorBodyHandler
import ai.hanzo.api.core.handlers.errorHandler
import ai.hanzo.api.core.handlers.jsonHandler
-import ai.hanzo.api.core.handlers.withErrorHandler
import ai.hanzo.api.core.http.HttpMethod
import ai.hanzo.api.core.http.HttpRequest
+import ai.hanzo.api.core.http.HttpResponse
import ai.hanzo.api.core.http.HttpResponse.Handler
import ai.hanzo.api.core.http.HttpResponseFor
import ai.hanzo.api.core.http.parseable
@@ -111,6 +111,7 @@ import ai.hanzo.api.services.blocking.UtilService
import ai.hanzo.api.services.blocking.UtilServiceImpl
import ai.hanzo.api.services.blocking.VertexAiService
import ai.hanzo.api.services.blocking.VertexAiServiceImpl
+import java.util.function.Consumer
class HanzoClientImpl(private val clientOptions: ClientOptions) : HanzoClient {
@@ -263,6 +264,9 @@ class HanzoClientImpl(private val clientOptions: ClientOptions) : HanzoClient {
override fun withRawResponse(): HanzoClient.WithRawResponse = withRawResponse
+ override fun withOptions(modifier: Consumer): HanzoClient =
+ HanzoClientImpl(clientOptions.toBuilder().apply(modifier::accept).build())
+
override fun models(): ModelService = models
override fun openai(): OpenAIService = openai
@@ -366,12 +370,13 @@ class HanzoClientImpl(private val clientOptions: ClientOptions) : HanzoClient {
// get /
withRawResponse().getHome(params, requestOptions).parse()
- override fun close() = clientOptions.httpClient.close()
+ override fun close() = clientOptions.close()
class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
HanzoClient.WithRawResponse {
- private val errorHandler: Handler = errorHandler(clientOptions.jsonMapper)
+ private val errorHandler: Handler =
+ errorHandler(errorBodyHandler(clientOptions.jsonMapper))
private val models: ModelService.WithRawResponse by lazy {
ModelServiceImpl.WithRawResponseImpl(clientOptions)
@@ -565,6 +570,13 @@ class HanzoClientImpl(private val clientOptions: ClientOptions) : HanzoClient {
BudgetServiceImpl.WithRawResponseImpl(clientOptions)
}
+ override fun withOptions(
+ modifier: Consumer
+ ): HanzoClient.WithRawResponse =
+ HanzoClientImpl.WithRawResponseImpl(
+ clientOptions.toBuilder().apply(modifier::accept).build()
+ )
+
override fun models(): ModelService.WithRawResponse = models
override fun openai(): OpenAIService.WithRawResponse = openai
@@ -663,7 +675,6 @@ class HanzoClientImpl(private val clientOptions: ClientOptions) : HanzoClient {
private val getHomeHandler: Handler =
jsonHandler(clientOptions.jsonMapper)
- .withErrorHandler(errorHandler)
override fun getHome(
params: ClientGetHomeParams,
@@ -672,12 +683,13 @@ class HanzoClientImpl(private val clientOptions: ClientOptions) : HanzoClient {
val request =
HttpRequest.builder()
.method(HttpMethod.GET)
+ .baseUrl(clientOptions.baseUrl())
.addPathSegments("")
.build()
.prepare(clientOptions, params)
val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions))
val response = clientOptions.httpClient.execute(request, requestOptions)
- return response.parseable {
+ return errorHandler.handle(response).parseable {
response
.use { getHomeHandler.handle(it) }
.also {
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/BaseDeserializer.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/BaseDeserializer.kt
index 6d563247..e05d3136 100644
--- a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/BaseDeserializer.kt
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/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
@@ -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/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Check.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Check.kt
index 48b8a942..56ae907e 100644
--- a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Check.kt
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Check.kt
@@ -5,6 +5,9 @@ package ai.hanzo.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" }
@@ -47,6 +50,7 @@ internal fun checkMaxLength(name: String, value: String, maxLength: Int): String
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"
@@ -55,12 +59,13 @@ internal fun checkJacksonVersionCompatibility() {
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 depends on Jackson version $MINIMUM_JACKSON_VERSION, but the following incompatible Jackson versions were detected at runtime:
+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)"
@@ -71,12 +76,16 @@ This can happen if you are either:
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/hanzoai/java-sdk#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,
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/ClientOptions.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/ClientOptions.kt
index e67044be..66631ea7 100644
--- a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/ClientOptions.kt
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/ClientOptions.kt
@@ -9,20 +9,91 @@ import ai.hanzo.api.core.http.QueryParams
import ai.hanzo.api.core.http.RetryingHttpClient
import com.fasterxml.jackson.databind.json.JsonMapper
import java.time.Clock
+import java.time.Duration
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+/** A class representing the SDK client configuration. */
class ClientOptions
private constructor(
private val originalHttpClient: HttpClient,
+ /**
+ * The HTTP client to use in the SDK.
+ *
+ * Use the one published in `hanzo-java-client-okhttp` or implement your own.
+ *
+ * This class takes ownership of the client and closes it when closed.
+ */
@get:JvmName("httpClient") val httpClient: HttpClient,
+ /**
+ * 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.
+ */
@get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean,
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [ai.hanzo.api.core.jsonMapper]. The default is usually sufficient and rarely
+ * needs to be overridden.
+ */
@get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
+ /**
+ * The interface to use for delaying execution, like during retries.
+ *
+ * This is primarily useful for using fake delays in tests.
+ *
+ * Defaults to real execution delays.
+ *
+ * This class takes ownership of the sleeper and closes it when closed.
+ */
+ @get:JvmName("sleeper") val sleeper: Sleeper,
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
@get:JvmName("clock") val clock: Clock,
- @get:JvmName("baseUrl") val baseUrl: String,
+ private val baseUrl: String?,
+ /** Headers to send with the request. */
@get:JvmName("headers") val headers: Headers,
+ /** Query params to send with the request. */
@get:JvmName("queryParams") val queryParams: QueryParams,
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
@get:JvmName("responseValidation") val responseValidation: Boolean,
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
@get:JvmName("timeout") val timeout: Timeout,
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * 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 request.
+ *
+ * Defaults to 2.
+ */
@get:JvmName("maxRetries") val maxRetries: Int,
+ /** The default name of the subscription key header of Azure */
@get:JvmName("apiKey") val apiKey: String,
) {
@@ -32,6 +103,16 @@ private constructor(
}
}
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.hanzo.ai`.
+ *
+ * The following other environments, with dedicated builder methods, are available:
+ * - sandbox: `https://api.sandbox.hanzo.ai`
+ */
+ fun baseUrl(): String = baseUrl ?: PRODUCTION_URL
+
fun toBuilder() = Builder().from(this)
companion object {
@@ -51,6 +132,11 @@ private constructor(
*/
@JvmStatic fun builder() = Builder()
+ /**
+ * Returns options configured using system properties and environment variables.
+ *
+ * @see Builder.fromEnv
+ */
@JvmStatic fun fromEnv(): ClientOptions = builder().fromEnv().build()
}
@@ -60,8 +146,9 @@ private constructor(
private var httpClient: HttpClient? = null
private var checkJacksonVersionCompatibility: Boolean = true
private var jsonMapper: JsonMapper = jsonMapper()
+ private var sleeper: Sleeper? = null
private var clock: Clock = Clock.systemUTC()
- private var baseUrl: String = PRODUCTION_URL
+ private var baseUrl: String? = null
private var headers: Headers.Builder = Headers.builder()
private var queryParams: QueryParams.Builder = QueryParams.builder()
private var responseValidation: Boolean = false
@@ -74,6 +161,7 @@ private constructor(
httpClient = clientOptions.originalHttpClient
checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
jsonMapper = clientOptions.jsonMapper
+ sleeper = clientOptions.sleeper
clock = clientOptions.clock
baseUrl = clientOptions.baseUrl
headers = clientOptions.headers.toBuilder()
@@ -84,26 +172,117 @@ private constructor(
apiKey = clientOptions.apiKey
}
- fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
+ /**
+ * The HTTP client to use in the SDK.
+ *
+ * Use the one published in `hanzo-java-client-okhttp` or implement your own.
+ *
+ * This class takes ownership of the client and closes it when closed.
+ */
+ fun httpClient(httpClient: HttpClient) = apply {
+ this.httpClient = PhantomReachableClosingHttpClient(httpClient)
+ }
+ /**
+ * 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 {
this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility
}
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [ai.hanzo.api.core.jsonMapper]. The default is usually sufficient and rarely
+ * needs to be overridden.
+ */
fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }
+ /**
+ * The interface to use for delaying execution, like during retries.
+ *
+ * This is primarily useful for using fake delays in tests.
+ *
+ * Defaults to real execution delays.
+ *
+ * This class takes ownership of the sleeper and closes it when closed.
+ */
+ fun sleeper(sleeper: Sleeper) = apply { this.sleeper = PhantomReachableSleeper(sleeper) }
+
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
fun clock(clock: Clock) = apply { this.clock = clock }
- fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl }
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.hanzo.ai`.
+ *
+ * The following other environments, with dedicated builder methods, are available:
+ * - sandbox: `https://api.sandbox.hanzo.ai`
+ */
+ fun baseUrl(baseUrl: String?) = apply { this.baseUrl = baseUrl }
+
+ /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */
+ fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull())
+
+ /** Sets [baseUrl] to `https://api.sandbox.hanzo.ai`. */
+ fun sandbox() = baseUrl(SANDBOX_URL)
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
fun responseValidation(responseValidation: Boolean) = apply {
this.responseValidation = responseValidation
}
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
fun timeout(timeout: Timeout) = apply { 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())
+
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * 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 request.
+ *
+ * Defaults to 2.
+ */
fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
+ /** The default name of the subscription key header of Azure */
fun apiKey(apiKey: String) = apply { this.apiKey = apiKey }
fun headers(headers: Headers) = apply {
@@ -186,7 +365,28 @@ private constructor(
fun removeAllQueryParams(keys: Set) = apply { queryParams.removeAll(keys) }
- fun fromEnv() = apply { System.getenv("HANZO_API_KEY")?.let { apiKey(it) } }
+ fun timeout(): Timeout = timeout
+
+ /**
+ * Updates configuration using system properties and environment variables.
+ *
+ * See this table for the available options:
+ *
+ * | Setter | System property | Environment variable | Required | Default value |
+ * |-----------|-----------------|----------------------|----------|--------------------------|
+ * | `apiKey` | `hanzo.apiKey` | `HANZO_API_KEY` | true | - |
+ * | `baseUrl` | `hanzo.baseUrl` | `HANZO_BASE_URL` | true | `"https://api.hanzo.ai"` |
+ *
+ * System properties take precedence over environment variables.
+ */
+ fun fromEnv() = apply {
+ (System.getProperty("hanzo.baseUrl") ?: System.getenv("HANZO_BASE_URL"))?.let {
+ baseUrl(it)
+ }
+ (System.getProperty("hanzo.apiKey") ?: System.getenv("HANZO_API_KEY"))?.let {
+ apiKey(it)
+ }
+ }
/**
* Returns an immutable instance of [ClientOptions].
@@ -203,6 +403,7 @@ private constructor(
*/
fun build(): ClientOptions {
val httpClient = checkRequired("httpClient", httpClient)
+ val sleeper = sleeper ?: PhantomReachableSleeper(DefaultSleeper())
val apiKey = checkRequired("apiKey", apiKey)
val headers = Headers.builder()
@@ -214,9 +415,10 @@ private constructor(
headers.put("X-Stainless-Package-Version", getPackageVersion())
headers.put("X-Stainless-Runtime", "JRE")
headers.put("X-Stainless-Runtime-Version", getJavaVersion())
+ headers.put("X-Stainless-Kotlin-Version", KotlinVersion.CURRENT.toString())
apiKey.let {
if (!it.isEmpty()) {
- headers.put("Ocp-Apim-Subscription-Key", it)
+ headers.put("x-litellm-api-key", it)
}
}
headers.replaceAll(this.headers.build())
@@ -224,15 +426,15 @@ private constructor(
return ClientOptions(
httpClient,
- PhantomReachableClosingHttpClient(
- RetryingHttpClient.builder()
- .httpClient(httpClient)
- .clock(clock)
- .maxRetries(maxRetries)
- .build()
- ),
+ RetryingHttpClient.builder()
+ .httpClient(httpClient)
+ .sleeper(sleeper)
+ .clock(clock)
+ .maxRetries(maxRetries)
+ .build(),
checkJacksonVersionCompatibility,
jsonMapper,
+ sleeper,
clock,
baseUrl,
headers.build(),
@@ -244,4 +446,19 @@ private constructor(
)
}
}
+
+ /**
+ * Closes these client options, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client options are
+ * 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 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() {
+ httpClient.close()
+ sleeper.close()
+ }
}
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/DefaultSleeper.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/DefaultSleeper.kt
new file mode 100644
index 00000000..6b15ec7e
--- /dev/null
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/DefaultSleeper.kt
@@ -0,0 +1,28 @@
+package ai.hanzo.api.core
+
+import java.time.Duration
+import java.util.Timer
+import java.util.TimerTask
+import java.util.concurrent.CompletableFuture
+
+class DefaultSleeper : Sleeper {
+
+ private val timer = Timer("DefaultSleeper", true)
+
+ override fun sleep(duration: Duration) = Thread.sleep(duration.toMillis())
+
+ override fun sleepAsync(duration: Duration): CompletableFuture {
+ val future = CompletableFuture()
+ timer.schedule(
+ object : TimerTask() {
+ override fun run() {
+ future.complete(null)
+ }
+ },
+ duration.toMillis(),
+ )
+ return future
+ }
+
+ override fun close() = timer.cancel()
+}
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/ObjectMappers.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/ObjectMappers.kt
index 6e6fc7e3..9c610872 100644
--- a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/ObjectMappers.kt
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/ObjectMappers.kt
@@ -4,23 +4,102 @@ package ai.hanzo.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.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.kotlinModule
import java.io.InputStream
+import java.time.DateTimeException
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.OffsetDateTime
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoField
fun jsonMapper(): JsonMapper =
JsonMapper.builder()
.addModule(kotlinModule())
.addModule(Jdk8Module())
.addModule(JavaTimeModule())
- .addModule(SimpleModule().addSerializer(InputStreamJsonSerializer))
+ .addModule(
+ SimpleModule()
+ .addSerializer(InputStreamSerializer)
+ .addDeserializer(OffsetDateTime::class.java, LenientOffsetDateTimeDeserializer())
+ )
+ .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.Float, 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.DateTime) {
+ it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Array) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .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)
@@ -34,7 +113,10 @@ fun jsonMapper(): JsonMapper =
.disable(MapperFeature.AUTO_DETECT_SETTERS)
.build()
-private object InputStreamJsonSerializer : BaseSerializer(InputStream::class) {
+/** A serializer that serializes [InputStream] to bytes. */
+private object InputStreamSerializer : BaseSerializer(InputStream::class) {
+
+ private fun readResolve(): Any = InputStreamSerializer
override fun serialize(
value: InputStream?,
@@ -48,3 +130,49 @@ private object InputStreamJsonSerializer : BaseSerializer(InputStre
}
}
}
+
+/**
+ * A deserializer that can deserialize [OffsetDateTime] from datetimes, dates, and zoned datetimes.
+ */
+private class LenientOffsetDateTimeDeserializer :
+ StdDeserializer(OffsetDateTime::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): OffsetDateTime {
+ 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()
+ .atZone(ZoneId.of("UTC"))
+ .toOffsetDateTime()
+ !temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
+ LocalDateTime.from(temporal).atZone(ZoneId.of("UTC")).toOffsetDateTime()
+ else -> OffsetDateTime.from(temporal)
+ }
+ } catch (e: DateTimeException) {
+ exceptions.add(e)
+ }
+ }
+
+ throw JsonParseException(p, "Cannot parse `OffsetDateTime` from value: ${p.text}").apply {
+ exceptions.forEach { addSuppressed(it) }
+ }
+ }
+}
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/PhantomReachableExecutorService.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/PhantomReachableExecutorService.kt
new file mode 100644
index 00000000..a95108d7
--- /dev/null
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/PhantomReachableExecutorService.kt
@@ -0,0 +1,58 @@
+package ai.hanzo.api.core
+
+import java.util.concurrent.Callable
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+
+/**
+ * A delegating wrapper around an [ExecutorService] that shuts it down once it's only phantom
+ * reachable.
+ *
+ * This class ensures the [ExecutorService] is shut down even if the user forgets to do it.
+ */
+internal class PhantomReachableExecutorService(private val executorService: ExecutorService) :
+ ExecutorService {
+ init {
+ closeWhenPhantomReachable(this) { executorService.shutdown() }
+ }
+
+ override fun execute(command: Runnable) = executorService.execute(command)
+
+ override fun shutdown() = executorService.shutdown()
+
+ override fun shutdownNow(): MutableList = executorService.shutdownNow()
+
+ override fun isShutdown(): Boolean = executorService.isShutdown
+
+ override fun isTerminated(): Boolean = executorService.isTerminated
+
+ override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean =
+ executorService.awaitTermination(timeout, unit)
+
+ override fun submit(task: Callable): Future = executorService.submit(task)
+
+ override fun submit(task: Runnable, result: T): Future =
+ executorService.submit(task, result)
+
+ override fun submit(task: Runnable): Future<*> = executorService.submit(task)
+
+ override fun invokeAll(
+ tasks: MutableCollection>
+ ): MutableList> = executorService.invokeAll(tasks)
+
+ override fun invokeAll(
+ tasks: MutableCollection>,
+ timeout: Long,
+ unit: TimeUnit,
+ ): MutableList> = executorService.invokeAll(tasks, timeout, unit)
+
+ override fun invokeAny(tasks: MutableCollection>): T =
+ executorService.invokeAny(tasks)
+
+ override fun invokeAny(
+ tasks: MutableCollection>,
+ timeout: Long,
+ unit: TimeUnit,
+ ): T = executorService.invokeAny(tasks, timeout, unit)
+}
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/PhantomReachableSleeper.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/PhantomReachableSleeper.kt
new file mode 100644
index 00000000..5effc26d
--- /dev/null
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/PhantomReachableSleeper.kt
@@ -0,0 +1,23 @@
+package ai.hanzo.api.core
+
+import java.time.Duration
+import java.util.concurrent.CompletableFuture
+
+/**
+ * A delegating wrapper around a [Sleeper] that closes it once it's only phantom reachable.
+ *
+ * This class ensures the [Sleeper] is closed even if the user forgets to do it.
+ */
+internal class PhantomReachableSleeper(private val sleeper: Sleeper) : Sleeper {
+
+ init {
+ closeWhenPhantomReachable(this, sleeper)
+ }
+
+ override fun sleep(duration: Duration) = sleeper.sleep(duration)
+
+ override fun sleepAsync(duration: Duration): CompletableFuture =
+ sleeper.sleepAsync(duration)
+
+ override fun close() = sleeper.close()
+}
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Properties.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Properties.kt
index decba931..70e2d21a 100644
--- a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Properties.kt
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Properties.kt
@@ -2,7 +2,7 @@
package ai.hanzo.api.core
-import java.util.Properties
+import ai.hanzo.api.client.HanzoClient
fun getOsArch(): String {
val osArch = System.getProperty("os.arch")
@@ -16,7 +16,7 @@ fun getOsArch(): String {
"x86_64" -> "x64"
"arm" -> "arm"
"aarch64" -> "arm64"
- else -> "other:${osArch}"
+ else -> "other:$osArch"
}
}
@@ -30,13 +30,13 @@ fun getOsName(): String {
osName.startsWith("Linux") -> "Linux"
osName.startsWith("Mac OS") -> "MacOS"
osName.startsWith("Windows") -> "Windows"
- else -> "Other:${osName}"
+ else -> "Other:$osName"
}
}
fun getOsVersion(): String = System.getProperty("os.version", "unknown")
fun getPackageVersion(): String =
- Properties::class.java.`package`.implementationVersion ?: "unknown"
+ HanzoClient::class.java.`package`.implementationVersion ?: "unknown"
fun getJavaVersion(): String = System.getProperty("java.version", "unknown")
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Sleeper.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Sleeper.kt
new file mode 100644
index 00000000..e08c7205
--- /dev/null
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Sleeper.kt
@@ -0,0 +1,21 @@
+package ai.hanzo.api.core
+
+import java.time.Duration
+import java.util.concurrent.CompletableFuture
+
+/**
+ * An interface for delaying execution for a specified amount of time.
+ *
+ * Useful for testing and cleaning up resources.
+ */
+interface Sleeper : AutoCloseable {
+
+ /** Synchronously pauses execution for the given [duration]. */
+ fun sleep(duration: Duration)
+
+ /** Asynchronously pauses execution for the given [duration]. */
+ fun sleepAsync(duration: Duration): CompletableFuture
+
+ /** Overridden from [AutoCloseable] to not have a checked exception in its signature. */
+ override fun close()
+}
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Timeout.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Timeout.kt
index 844fb638..ba32882f 100644
--- a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Timeout.kt
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Timeout.kt
@@ -157,10 +157,14 @@ private constructor(
return true
}
- return /* spotless:off */ other is Timeout && connect == other.connect && read == other.read && write == other.write && request == other.request /* spotless:on */
+ return other is Timeout &&
+ connect == other.connect &&
+ read == other.read &&
+ write == other.write &&
+ request == other.request
}
- override fun hashCode(): Int = /* spotless:off */ Objects.hash(connect, read, write, request) /* spotless:on */
+ override fun hashCode(): Int = Objects.hash(connect, read, write, request)
override fun toString() =
"Timeout{connect=$connect, read=$read, write=$write, request=$request}"
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Utils.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Utils.kt
index 5348d056..75457e0c 100644
--- a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Utils.kt
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Utils.kt
@@ -5,6 +5,8 @@ package ai.hanzo.api.core
import ai.hanzo.api.errors.HanzoInvalidDataException
import java.util.Collections
import java.util.SortedMap
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.locks.Lock
@JvmSynthetic
internal fun T?.getOrThrow(name: String): T =
@@ -25,6 +27,34 @@ internal fun , V> SortedMap.toImmutable(): SortedMap> 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].
*
@@ -62,3 +92,24 @@ internal fun Any?.contentToString(): String {
}
internal interface Enum
+
+/**
+ * Executes the given [action] while holding the lock, returning a [CompletableFuture] with the
+ * result.
+ *
+ * @param action The asynchronous action to execute while holding the lock
+ * @return A [CompletableFuture] that completes with the result of the action
+ */
+@JvmSynthetic
+internal fun Lock.withLockAsync(action: () -> CompletableFuture): CompletableFuture {
+ lock()
+ val future =
+ try {
+ action()
+ } catch (e: Throwable) {
+ unlock()
+ throw e
+ }
+ future.whenComplete { _, _ -> unlock() }
+ return future
+}
diff --git a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Values.kt b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Values.kt
index a7334087..0e5ad6e4 100644
--- a/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Values.kt
+++ b/hanzo-java-core/src/main/kotlin/ai/hanzo/api/core/Values.kt
@@ -29,30 +29,51 @@ import java.io.InputStream
import java.util.Objects
import java.util.Optional
+/**
+ * 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)
@@ -60,6 +81,12 @@ sealed class JsonField {
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)
@@ -67,6 +94,12 @@ sealed class JsonField {
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)
@@ -77,6 +110,12 @@ sealed class JsonField {
fun asStringOrThrow(): String =
asString().orElseThrow { HanzoInvalidDataException("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)
@@ -95,6 +134,12 @@ sealed class JsonField {
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