diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 59565e8e..65f558e7 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "1.6.1"
+ ".": "2.0.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 089abe5d..297d33cc 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 95
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-0ee6b36cf3cc278cef4199a6aec5f7d530a6c1f17a74830037e96d50ca1edc50.yml
-openapi_spec_hash: e8ec5f46bc0655b34f292422d58a60f6
-config_hash: d9b6b6e6bc85744663e300eebc482067
+configured_endpoints: 99
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-d51538ac955164de98b0c94a0a4718d96623fe39bf31a1d168be06c93c94e645.yml
+openapi_spec_hash: 33e00a48df8f94c94f46290c489f132b
+config_hash: c42d37618b8628ce7e1c76437db5dd8f
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f164c88f..b882e401 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,54 @@
# Changelog
+## 2.0.0 (2025-05-21)
+
+Full Changelog: [v1.6.1...v2.0.0](https://github.com/openai/openai-java/compare/v1.6.1...v2.0.0)
+
+### ⚠ BREAKING CHANGES
+
+* **client:** change precision of some numeric types
+* **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:** Add reinforcement fine-tuning api support ([3a9fcbb](https://github.com/openai/openai-java/commit/3a9fcbb2d47a6b106d963a4243c255d55123ff9c))
+* **api:** further updates for evals API ([2b9d5bc](https://github.com/openai/openai-java/commit/2b9d5bc92d4e67cfc5b48343bd999ccbe87ca2e7))
+* **api:** manual updates ([005a643](https://github.com/openai/openai-java/commit/005a6439977311990dca20e75783657e90aa3898))
+* **api:** responses x eval api ([66327c5](https://github.com/openai/openai-java/commit/66327c540652f63147158fc9ae8ffdd5e75cbca4))
+* **api:** Updating Assistants and Evals API schemas ([02c6df6](https://github.com/openai/openai-java/commit/02c6df6457c3e03a8ae87d8b895a4838809947f3))
+* **client:** allow providing some params positionally ([1c6e875](https://github.com/openai/openai-java/commit/1c6e8759bf7e6b8a7e3b245c5a2365edf2388fca))
+* **client:** extract auto pagination to shared classes ([855d571](https://github.com/openai/openai-java/commit/855d571da1cc772b31493f6a36032934772c3757))
+* **client:** type safe structured outputs ([#463](https://github.com/openai/openai-java/issues/463)) ([e123fdd](https://github.com/openai/openai-java/commit/e123fdd3f75980b7edea84eaf6f6c101cb9a2ea2))
+
+
+### Bug Fixes
+
+* add missing `deploymentModel` params ([d9af1fb](https://github.com/openai/openai-java/commit/d9af1fbe5309ae7da0522895596573fc36162387))
+* **client:** properly support srt and vtt in audio transcriptions. ([#472](https://github.com/openai/openai-java/issues/472)) ([1e5bf3d](https://github.com/openai/openai-java/commit/1e5bf3d36cb100d1b1ad8ac328b99cc50d16e4da))
+* merge conflict ([bf7e961](https://github.com/openai/openai-java/commit/bf7e961819911f28f5ad0aaa03cda05ccfa5dbed))
+* missing validity ([301a38a](https://github.com/openai/openai-java/commit/301a38adfa5782238e407f9ea22dd9635baa4e4b))
+
+
+### Chores
+
+* **docs:** grammar improvements ([13b0fbc](https://github.com/openai/openai-java/commit/13b0fbc79ec365433ac33ccd7c5fef3e9b858fae))
+* **internal:** fix custom code ([567c86e](https://github.com/openai/openai-java/commit/567c86e958acf5898ee0be7547cfa0da34f07eb2))
+
+
+### Documentation
+
+* remove or fix invalid readme examples ([fa9f7fc](https://github.com/openai/openai-java/commit/fa9f7fc1e68b8754a50373c455cad45f59b9bda3))
+
+
+### Refactors
+
+* **client:** change precision of some numeric types ([291b0f4](https://github.com/openai/openai-java/commit/291b0f4ac12f61f31c40a80a46db2cffc9012a9a))
+
## 1.6.1 (2025-05-08)
Full Changelog: [v1.6.0...v1.6.1](https://github.com/openai/openai-java/compare/v1.6.0...v1.6.1)
diff --git a/README.md b/README.md
index 6a05116a..f267b450 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
-[](https://central.sonatype.com/artifact/com.openai/openai-java/1.6.1)
-[](https://javadoc.io/doc/com.openai/openai-java/1.6.1)
+[](https://central.sonatype.com/artifact/com.openai/openai-java/2.0.0)
+[](https://javadoc.io/doc/com.openai/openai-java/2.0.0)
@@ -11,7 +11,7 @@ The OpenAI Java SDK provides convenient access to the [OpenAI REST API](https://
-The REST API documentation can be found on [platform.openai.com](https://platform.openai.com/docs). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.openai/openai-java/1.6.1).
+The REST API documentation can be found on [platform.openai.com](https://platform.openai.com/docs). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.openai/openai-java/2.0.0).
@@ -22,7 +22,7 @@ The REST API documentation can be found on [platform.openai.com](https://platfor
### Gradle
```kotlin
-implementation("com.openai:openai-java:1.6.1")
+implementation("com.openai:openai-java:2.0.0")
```
### Maven
@@ -31,7 +31,7 @@ implementation("com.openai:openai-java:1.6.1")
com.openai
openai-java
- 1.6.1
+ 2.0.0
```
@@ -286,7 +286,7 @@ OpenAIClient client = OpenAIOkHttpClient.builder()
The SDK provides conveniences for streamed chat completions. A
[`ChatCompletionAccumulator`](openai-java-core/src/main/kotlin/com/openai/helpers/ChatCompletionAccumulator.kt)
-can record the stream of chat completion chunks in the response as they are processed and accumulate
+can record the stream of chat completion chunks in the response as they are processed and accumulate
a [`ChatCompletion`](openai-java-core/src/main/kotlin/com/openai/models/chat/completions/ChatCompletion.kt)
object similar to that which would have been returned by the non-streaming API.
@@ -334,6 +334,205 @@ client.chat()
ChatCompletion chatCompletion = chatCompletionAccumulator.chatCompletion();
```
+## Structured outputs with JSON schemas
+
+Open AI [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs?api-mode=chat)
+is a feature that ensures that the model will always generate responses that adhere to a supplied
+[JSON schema](https://json-schema.org/overview/what-is-jsonschema).
+
+A JSON schema can be defined by creating a
+[`ResponseFormatJsonSchema`](openai-java-core/src/main/kotlin/com/openai/models/ResponseFormatJsonSchema.kt)
+and setting it on the input parameters. However, for greater convenience, a JSON schema can instead
+be derived automatically from the structure of an arbitrary Java class. The JSON content from the
+response will then be converted automatically to an instance of that Java class. A full, working
+example of the use of Structured Outputs with arbitrary Java classes can be seen in
+[`StructuredOutputsExample`](openai-java-example/src/main/java/com/openai/example/StructuredOutputsExample.java).
+
+Java classes can contain fields declared to be instances of other classes and can use collections:
+
+```java
+class Person {
+ public String name;
+ public int birthYear;
+}
+
+class Book {
+ public String title;
+ public Person author;
+ public int publicationYear;
+}
+
+class BookList {
+ public List books;
+}
+```
+
+Pass the top-level class—`BookList` in this example—to `responseFormat(Class)` when building the
+parameters and then access an instance of `BookList` from the generated message content in the
+response:
+
+```java
+import com.openai.models.ChatModel;
+import com.openai.models.chat.completions.ChatCompletionCreateParams;
+import com.openai.models.chat.completions.StructuredChatCompletionCreateParams;
+
+StructuredChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
+ .addUserMessage("List some famous late twentieth century novels.")
+ .model(ChatModel.GPT_4_1)
+ .responseFormat(BookList.class)
+ .build();
+
+client.chat().completions().create(params).choices().stream()
+ .flatMap(choice -> choice.message().content().stream())
+ .flatMap(bookList -> bookList.books.stream())
+ .forEach(book -> System.out.println(book.title + " by " + book.author.name));
+```
+
+You can start building the parameters with an instance of
+[`ChatCompletionCreateParams.Builder`](openai-java-core/src/main/kotlin/com/openai/models/chat/completions/ChatCompletionCreateParams.kt)
+or
+[`StructuredChatCompletionCreateParams.Builder`](openai-java-core/src/main/kotlin/com/openai/models/chat/completions/StructuredChatCompletionCreateParams.kt).
+If you start with the former (which allows for more compact code) the builder type will change to
+the latter when `ChatCompletionCreateParams.Builder.responseFormat(Class)` is called.
+
+If a field in a class is optional and does not require a defined value, you can represent this using
+the [`java.util.Optional`](https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html) class.
+It is up to the AI model to decide whether to provide a value for that field or leave it empty.
+
+```java
+import java.util.Optional;
+
+class Book {
+ public String title;
+ public Person author;
+ public int publicationYear;
+ public Optional isbn;
+}
+```
+
+Generic type information for fields is retained in the class's metadata, but _generic type erasure_
+applies in other scopes. While, for example, a JSON schema defining an array of books can be derived
+from the `BookList.books` field with type `List`, a valid JSON schema cannot be derived from a
+local variable of that same type, so the following will _not_ work:
+
+```java
+List books = new ArrayList<>();
+
+StructuredChatCompletionCreateParams> params = ChatCompletionCreateParams.builder()
+ .responseFormat(books.getClass())
+ // ...
+ .build();
+```
+
+If an error occurs while converting a JSON response to an instance of a Java class, the error
+message will include the JSON response to assist in diagnosis. For instance, if the response is
+truncated, the JSON data will be incomplete and cannot be converted to a class instance. If your
+JSON response may contain sensitive information, avoid logging it directly, or ensure that you
+redact any sensitive details from the error message.
+
+### Local JSON schema validation
+
+Structured Outputs supports a
+[subset](https://platform.openai.com/docs/guides/structured-outputs#supported-schemas) of the JSON
+Schema language. Schemas are generated automatically from classes to align with this subset.
+However, due to the inherent structure of the classes, the generated schema may still violate
+certain OpenAI schema restrictions, such as exceeding the maximum nesting depth or utilizing
+unsupported data types.
+
+To facilitate compliance, the method `responseFormat(Class)` performs a validation check on the
+schema derived from the specified class. This validation ensures that all restrictions are adhered
+to. If any issues are detected, an exception will be thrown, providing a detailed message outlining
+the reasons for the validation failure.
+
+- **Local Validation**: The validation process occurs locally, meaning no requests are sent to the
+remote AI model. If the schema passes local validation, it is likely to pass remote validation as
+well.
+- **Remote Validation**: The remote AI model will conduct its own validation upon receiving the JSON
+schema in the request.
+- **Version Compatibility**: There may be instances where local validation fails while remote
+validation succeeds. This can occur if the SDK version is outdated compared to the restrictions
+enforced by the remote AI model.
+- **Disabling Local Validation**: If you encounter compatibility issues and wish to bypass local
+validation, you can disable it by passing
+[`JsonSchemaLocalValidation.NO`](openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaLocalValidation.kt)
+to the `responseFormat(Class, JsonSchemaLocalValidation)` method when building the parameters.
+(The default value for this parameter is `JsonSchemaLocalValidation.YES`.)
+
+```java
+import com.openai.core.JsonSchemaLocalValidation;
+import com.openai.models.ChatModel;
+import com.openai.models.chat.completions.ChatCompletionCreateParams;
+import com.openai.models.chat.completions.StructuredChatCompletionCreateParams;
+
+StructuredChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
+ .addUserMessage("List some famous late twentieth century novels.")
+ .model(ChatModel.GPT_4_1)
+ .responseFormat(BookList.class, JsonSchemaLocalValidation.NO)
+ .build();
+```
+
+By following these guidelines, you can ensure that your structured outputs conform to the necessary
+schema requirements and minimize the risk of remote validation errors.
+
+### Usage with the Responses API
+
+_Structured Outputs_ are also supported for the Responses API. The usage is the same as described
+except where the Responses API differs slightly from the Chat Completions API. Pass the top-level
+class to `text(Class)` when building the parameters and then access an instance of the class from
+the generated message content in the response.
+
+You can start building the parameters with an instance of
+[`ResponseCreateParams.Builder`](openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseCreateParams.kt)
+or
+[`StructuredResponseCreateParams.Builder`](openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseCreateParams.kt).
+If you start with the former (which allows for more compact code) the builder type will change to
+the latter when `ResponseCreateParams.Builder.text(Class)` is called.
+
+For a full example of the usage of _Structured Outputs_ with the Responses API, see
+[`ResponsesStructuredOutputsExample`](openai-java-example/src/main/java/com/openai/example/ResponsesStructuredOutputsExample.java).
+
+### Annotating classes and JSON schemas
+
+You can use annotations to add further information to the JSON schema derived from your Java
+classes, or to exclude individual fields from the schema. Details from annotations captured in the
+JSON schema may be used by the AI model to improve its response. The SDK supports the use of
+[Jackson Databind](https://github.com/FasterXML/jackson-databind) annotations.
+
+```java
+import com.fasterxml.jackson.annotation.JsonClassDescription;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+class Person {
+ @JsonPropertyDescription("The first name and surname of the person")
+ public String name;
+ public int birthYear;
+ @JsonPropertyDescription("The year the person died, or 'present' if the person is living.")
+ public String deathYear;
+}
+
+@JsonClassDescription("The details of one published book")
+class Book {
+ public String title;
+ public Person author;
+ @JsonPropertyDescription("The year in which the book was first published.")
+ public int publicationYear;
+ @JsonIgnore public String genre;
+}
+
+class BookList {
+ public List books;
+}
+```
+
+- Use `@JsonClassDescription` to add a detailed description to a class.
+- Use `@JsonPropertyDescription` to add a detailed description to a field of a class.
+- Use `@JsonIgnore` to omit a field of a class from the generated JSON schema.
+
+If you use `@JsonProperty(required = false)`, the `false` value will be ignored. OpenAI JSON schemas
+must mark all properties as _required_, so the schema generated from your Java classes will respect
+that restriction and ignore any annotation that would violate it.
+
## File uploads
The SDK defines methods that accept files.
@@ -412,10 +611,7 @@ These methods return [`HttpResponse`](openai-java-core/src/main/kotlin/com/opena
import com.openai.core.http.HttpResponse;
import com.openai.models.files.FileContentParams;
-FileContentParams params = FileContentParams.builder()
- .fileId("file_id")
- .build();
-HttpResponse response = client.files().content(params);
+HttpResponse response = client.files().content("file_id");
```
To save the response content to a file, use the [`Files.copy(...)`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#copy-java.io.InputStream-java.nio.file.Path-java.nio.file.CopyOption...-) method:
@@ -528,53 +724,101 @@ The SDK throws custom unchecked exception types:
## Pagination
-For methods that return a paginated list of results, this library provides convenient ways access the results either one page at a time, or item-by-item across all pages.
+The SDK defines methods that return a paginated lists of results. It provides convenient ways to access the results either one page at a time or item-by-item across all pages.
### Auto-pagination
-To iterate through all results across all pages, you can use `autoPager`, which automatically handles fetching more pages for you:
+To iterate through all results across all pages, use the `autoPager()` method, which automatically fetches more pages as needed.
-### Synchronous
+When using the synchronous client, the method returns an [`Iterable`](https://docs.oracle.com/javase/8/docs/api/java/lang/Iterable.html)
```java
import com.openai.models.finetuning.jobs.FineTuningJob;
import com.openai.models.finetuning.jobs.JobListPage;
-// As an Iterable:
-JobListPage page = client.fineTuning().jobs().list(params);
+JobListPage page = client.fineTuning().jobs().list();
+
+// Process as an Iterable
for (FineTuningJob job : page.autoPager()) {
System.out.println(job);
-};
+}
-// As a Stream:
-client.fineTuning().jobs().list(params).autoPager().stream()
+// Process as a Stream
+page.autoPager()
+ .stream()
.limit(50)
.forEach(job -> System.out.println(job));
```
-### Asynchronous
+When using the asynchronous client, the method returns an [`AsyncStreamResponse`](openai-java-core/src/main/kotlin/com/openai/core/http/AsyncStreamResponse.kt):
```java
-// Using forEach, which returns CompletableFuture:
-asyncClient.fineTuning().jobs().list(params).autoPager()
- .forEach(job -> System.out.println(job), executor);
+import com.openai.core.http.AsyncStreamResponse;
+import com.openai.models.finetuning.jobs.FineTuningJob;
+import com.openai.models.finetuning.jobs.JobListPageAsync;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+CompletableFuture pageFuture = client.async().fineTuning().jobs().list();
+
+pageFuture.thenRun(page -> page.autoPager().subscribe(job -> {
+ System.out.println(job);
+}));
+
+// If you need to handle errors or completion of the stream
+pageFuture.thenRun(page -> page.autoPager().subscribe(new AsyncStreamResponse.Handler<>() {
+ @Override
+ public void onNext(FineTuningJob job) {
+ System.out.println(job);
+ }
+
+ @Override
+ public void onComplete(Optional error) {
+ if (error.isPresent()) {
+ System.out.println("Something went wrong!");
+ throw new RuntimeException(error.get());
+ } else {
+ System.out.println("No more!");
+ }
+ }
+}));
+
+// Or use futures
+pageFuture.thenRun(page -> page.autoPager()
+ .subscribe(job -> {
+ System.out.println(job);
+ })
+ .onCompleteFuture()
+ .whenComplete((unused, error) -> {
+ if (error != null) {
+ System.out.println("Something went wrong!");
+ throw new RuntimeException(error);
+ } else {
+ System.out.println("No more!");
+ }
+ }));
```
### Manual pagination
-If none of the above helpers meet your needs, you can also manually request pages one-by-one. A page of results has a `data()` method to fetch the list of objects, as well as top-level `response` and other methods to fetch top-level data about the page. It also has methods `hasNextPage`, `getNextPage`, and `getNextPageParams` methods to help with pagination.
+To access individual page items and manually request the next page, use the `items()`,
+`hasNextPage()`, and `nextPage()` methods:
```java
import com.openai.models.finetuning.jobs.FineTuningJob;
import com.openai.models.finetuning.jobs.JobListPage;
-JobListPage page = client.fineTuning().jobs().list(params);
-while (page != null) {
- for (FineTuningJob job : page.data()) {
+JobListPage page = client.fineTuning().jobs().list();
+while (true) {
+ for (FineTuningJob job : page.items()) {
System.out.println(job);
}
- page = page.getNextPage().orElse(null);
+ if (!page.hasNextPage()) {
+ break;
+ }
+
+ page = page.nextPage();
}
```
@@ -607,7 +851,7 @@ If the SDK threw an exception, but you're _certain_ the version is compatible, t
## Microsoft Azure
-To use this library with [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/overview), use the same
+To use this library with [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/overview), use the same
OpenAI client builder but with the Azure-specific configuration.
```java
@@ -620,7 +864,7 @@ OpenAIClient client = OpenAIOkHttpClient.builder()
.build();
```
-See the complete Azure OpenAI example in the [`openai-java-example`](openai-java-example/src/main/java/com/openai/example/AzureEntraIdExample.java) directory. The other examples in the directory also work with Azure as long as the client is configured to use it.
+See the complete Azure OpenAI example in the [`openai-java-example`](openai-java-example/src/main/java/com/openai/example/AzureEntraIdExample.java) directory. The other examples in the directory also work with Azure as long as the client is configured to use it.
## Network options
@@ -657,9 +901,7 @@ Requests time out after 10 minutes by default.
To set a custom timeout, configure the method call using the `timeout` method:
```java
-import com.openai.models.ChatModel;
import com.openai.models.chat.completions.ChatCompletion;
-import com.openai.models.chat.completions.ChatCompletionCreateParams;
ChatCompletion chatCompletion = client.chat().completions().create(
params, RequestOptions.builder().timeout(Duration.ofSeconds(30)).build()
@@ -775,11 +1017,12 @@ To set a documented parameter or property to an undocumented or not yet supporte
```java
import com.openai.core.JsonValue;
+import com.openai.models.ChatModel;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
- .addUserMessage("Say this is a test")
- .model(JsonValue.from(42))
+ .messages(JsonValue.from(42))
+ .model(ChatModel.GPT_4_1)
.build();
```
@@ -909,9 +1152,7 @@ ChatCompletion chatCompletion = client.chat().completions().create(params).valid
Or configure the method call to validate the response using the `responseValidation` method:
```java
-import com.openai.models.ChatModel;
import com.openai.models.chat.completions.ChatCompletion;
-import com.openai.models.chat.completions.ChatCompletionCreateParams;
ChatCompletion chatCompletion = client.chat().completions().create(
params, RequestOptions.builder().responseValidation(true).build()
diff --git a/SECURITY.md b/SECURITY.md
index 3b3bd8a6..4adb0c54 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -16,13 +16,13 @@ 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 OpenAI please follow the respective company's security reporting guidelines.
+or products provided by OpenAI, please follow the respective company's security reporting guidelines.
### OpenAI Terms and Policies
Our Security Policy can be found at [Security Policy URL](https://openai.com/policies/coordinated-vulnerability-disclosure-policy).
-Please contact disclosure@openai.com for any questions or concerns regarding security of our services.
+Please contact disclosure@openai.com for any questions or concerns regarding the security of our services.
---
diff --git a/build.gradle.kts b/build.gradle.kts
index 47de6d33..581930ee 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,7 +8,7 @@ repositories {
allprojects {
group = "com.openai"
- version = "1.6.1" // x-release-please-version
+ version = "2.0.0" // x-release-please-version
}
subprojects {
diff --git a/openai-java-core/build.gradle.kts b/openai-java-core/build.gradle.kts
index 08a91e0d..894f0e23 100644
--- a/openai-java-core/build.gradle.kts
+++ b/openai-java-core/build.gradle.kts
@@ -27,6 +27,8 @@ dependencies {
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")
+ implementation("com.github.victools:jsonschema-generator:4.38.0")
+ implementation("com.github.victools:jsonschema-module-jackson:4.38.0")
testImplementation(kotlin("test"))
testImplementation(project(":openai-java-client-okhttp"))
diff --git a/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClient.kt b/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClient.kt
index 3995c9ea..e990bc66 100644
--- a/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClient.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClient.kt
@@ -11,6 +11,7 @@ import com.openai.services.blocking.EmbeddingService
import com.openai.services.blocking.EvalService
import com.openai.services.blocking.FileService
import com.openai.services.blocking.FineTuningService
+import com.openai.services.blocking.GraderService
import com.openai.services.blocking.ImageService
import com.openai.services.blocking.ModelService
import com.openai.services.blocking.ModerationService
@@ -65,6 +66,8 @@ interface OpenAIClient {
fun fineTuning(): FineTuningService
+ fun graders(): GraderService
+
fun vectorStores(): VectorStoreService
fun beta(): BetaService
@@ -111,6 +114,8 @@ interface OpenAIClient {
fun fineTuning(): FineTuningService.WithRawResponse
+ fun graders(): GraderService.WithRawResponse
+
fun vectorStores(): VectorStoreService.WithRawResponse
fun beta(): BetaService.WithRawResponse
diff --git a/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClientAsync.kt b/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClientAsync.kt
index 2a2d5894..aff5f61e 100644
--- a/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClientAsync.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClientAsync.kt
@@ -11,6 +11,7 @@ import com.openai.services.async.EmbeddingServiceAsync
import com.openai.services.async.EvalServiceAsync
import com.openai.services.async.FileServiceAsync
import com.openai.services.async.FineTuningServiceAsync
+import com.openai.services.async.GraderServiceAsync
import com.openai.services.async.ImageServiceAsync
import com.openai.services.async.ModelServiceAsync
import com.openai.services.async.ModerationServiceAsync
@@ -65,6 +66,8 @@ interface OpenAIClientAsync {
fun fineTuning(): FineTuningServiceAsync
+ fun graders(): GraderServiceAsync
+
fun vectorStores(): VectorStoreServiceAsync
fun beta(): BetaServiceAsync
@@ -111,6 +114,8 @@ interface OpenAIClientAsync {
fun fineTuning(): FineTuningServiceAsync.WithRawResponse
+ fun graders(): GraderServiceAsync.WithRawResponse
+
fun vectorStores(): VectorStoreServiceAsync.WithRawResponse
fun beta(): BetaServiceAsync.WithRawResponse
diff --git a/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClientAsyncImpl.kt b/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClientAsyncImpl.kt
index 2bbeb00b..5e2719a6 100644
--- a/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClientAsyncImpl.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClientAsyncImpl.kt
@@ -22,6 +22,8 @@ import com.openai.services.async.FileServiceAsync
import com.openai.services.async.FileServiceAsyncImpl
import com.openai.services.async.FineTuningServiceAsync
import com.openai.services.async.FineTuningServiceAsyncImpl
+import com.openai.services.async.GraderServiceAsync
+import com.openai.services.async.GraderServiceAsyncImpl
import com.openai.services.async.ImageServiceAsync
import com.openai.services.async.ImageServiceAsyncImpl
import com.openai.services.async.ModelServiceAsync
@@ -84,6 +86,10 @@ class OpenAIClientAsyncImpl(private val clientOptions: ClientOptions) : OpenAICl
FineTuningServiceAsyncImpl(clientOptionsWithUserAgent)
}
+ private val graders: GraderServiceAsync by lazy {
+ GraderServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
private val vectorStores: VectorStoreServiceAsync by lazy {
VectorStoreServiceAsyncImpl(clientOptionsWithUserAgent)
}
@@ -126,6 +132,8 @@ class OpenAIClientAsyncImpl(private val clientOptions: ClientOptions) : OpenAICl
override fun fineTuning(): FineTuningServiceAsync = fineTuning
+ override fun graders(): GraderServiceAsync = graders
+
override fun vectorStores(): VectorStoreServiceAsync = vectorStores
override fun beta(): BetaServiceAsync = beta
@@ -179,6 +187,10 @@ class OpenAIClientAsyncImpl(private val clientOptions: ClientOptions) : OpenAICl
FineTuningServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
+ private val graders: GraderServiceAsync.WithRawResponse by lazy {
+ GraderServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
private val vectorStores: VectorStoreServiceAsync.WithRawResponse by lazy {
VectorStoreServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
@@ -221,6 +233,8 @@ class OpenAIClientAsyncImpl(private val clientOptions: ClientOptions) : OpenAICl
override fun fineTuning(): FineTuningServiceAsync.WithRawResponse = fineTuning
+ override fun graders(): GraderServiceAsync.WithRawResponse = graders
+
override fun vectorStores(): VectorStoreServiceAsync.WithRawResponse = vectorStores
override fun beta(): BetaServiceAsync.WithRawResponse = beta
diff --git a/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClientImpl.kt b/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClientImpl.kt
index 17ad13b4..c79edfec 100644
--- a/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClientImpl.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/client/OpenAIClientImpl.kt
@@ -22,6 +22,8 @@ import com.openai.services.blocking.FileService
import com.openai.services.blocking.FileServiceImpl
import com.openai.services.blocking.FineTuningService
import com.openai.services.blocking.FineTuningServiceImpl
+import com.openai.services.blocking.GraderService
+import com.openai.services.blocking.GraderServiceImpl
import com.openai.services.blocking.ImageService
import com.openai.services.blocking.ImageServiceImpl
import com.openai.services.blocking.ModelService
@@ -78,6 +80,8 @@ class OpenAIClientImpl(private val clientOptions: ClientOptions) : OpenAIClient
FineTuningServiceImpl(clientOptionsWithUserAgent)
}
+ private val graders: GraderService by lazy { GraderServiceImpl(clientOptionsWithUserAgent) }
+
private val vectorStores: VectorStoreService by lazy {
VectorStoreServiceImpl(clientOptionsWithUserAgent)
}
@@ -116,6 +120,8 @@ class OpenAIClientImpl(private val clientOptions: ClientOptions) : OpenAIClient
override fun fineTuning(): FineTuningService = fineTuning
+ override fun graders(): GraderService = graders
+
override fun vectorStores(): VectorStoreService = vectorStores
override fun beta(): BetaService = beta
@@ -169,6 +175,10 @@ class OpenAIClientImpl(private val clientOptions: ClientOptions) : OpenAIClient
FineTuningServiceImpl.WithRawResponseImpl(clientOptions)
}
+ private val graders: GraderService.WithRawResponse by lazy {
+ GraderServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
private val vectorStores: VectorStoreService.WithRawResponse by lazy {
VectorStoreServiceImpl.WithRawResponseImpl(clientOptions)
}
@@ -211,6 +221,8 @@ class OpenAIClientImpl(private val clientOptions: ClientOptions) : OpenAIClient
override fun fineTuning(): FineTuningService.WithRawResponse = fineTuning
+ override fun graders(): GraderService.WithRawResponse = graders
+
override fun vectorStores(): VectorStoreService.WithRawResponse = vectorStores
override fun beta(): BetaService.WithRawResponse = beta
diff --git a/openai-java-core/src/main/kotlin/com/openai/core/AutoPager.kt b/openai-java-core/src/main/kotlin/com/openai/core/AutoPager.kt
new file mode 100644
index 00000000..888fd034
--- /dev/null
+++ b/openai-java-core/src/main/kotlin/com/openai/core/AutoPager.kt
@@ -0,0 +1,21 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.openai.core
+
+import java.util.stream.Stream
+import java.util.stream.StreamSupport
+
+class AutoPager private constructor(private val firstPage: Page) : Iterable {
+
+ companion object {
+
+ fun from(firstPage: Page): AutoPager = AutoPager(firstPage)
+ }
+
+ override fun iterator(): Iterator =
+ generateSequence(firstPage) { if (it.hasNextPage()) it.nextPage() else null }
+ .flatMap { it.items() }
+ .iterator()
+
+ fun stream(): Stream = StreamSupport.stream(spliterator(), false)
+}
diff --git a/openai-java-core/src/main/kotlin/com/openai/core/AutoPagerAsync.kt b/openai-java-core/src/main/kotlin/com/openai/core/AutoPagerAsync.kt
new file mode 100644
index 00000000..649be47c
--- /dev/null
+++ b/openai-java-core/src/main/kotlin/com/openai/core/AutoPagerAsync.kt
@@ -0,0 +1,88 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.openai.core
+
+import com.openai.core.http.AsyncStreamResponse
+import java.util.Optional
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.CompletionException
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicReference
+
+class AutoPagerAsync
+private constructor(private val firstPage: PageAsync, private val defaultExecutor: Executor) :
+ AsyncStreamResponse {
+
+ companion object {
+
+ fun from(firstPage: PageAsync, defaultExecutor: Executor): AutoPagerAsync =
+ AutoPagerAsync(firstPage, defaultExecutor)
+ }
+
+ private val onCompleteFuture = CompletableFuture()
+ private val state = AtomicReference(State.NEW)
+
+ override fun subscribe(handler: AsyncStreamResponse.Handler): AsyncStreamResponse =
+ subscribe(handler, defaultExecutor)
+
+ override fun subscribe(
+ handler: AsyncStreamResponse.Handler,
+ executor: Executor,
+ ): AsyncStreamResponse = apply {
+ // TODO(JDK): Use `compareAndExchange` once targeting JDK 9.
+ check(state.compareAndSet(State.NEW, State.SUBSCRIBED)) {
+ if (state.get() == State.SUBSCRIBED) "Cannot subscribe more than once"
+ else "Cannot subscribe after the response is closed"
+ }
+
+ fun PageAsync.handle(): CompletableFuture {
+ if (state.get() == State.CLOSED) {
+ return CompletableFuture.completedFuture(null)
+ }
+
+ items().forEach { handler.onNext(it) }
+ return if (hasNextPage()) nextPage().thenCompose { it.handle() }
+ else CompletableFuture.completedFuture(null)
+ }
+
+ executor.execute {
+ firstPage.handle().whenComplete { _, error ->
+ val actualError =
+ if (error is CompletionException && error.cause != null) error.cause else error
+ try {
+ handler.onComplete(Optional.ofNullable(actualError))
+ } finally {
+ try {
+ if (actualError == null) {
+ onCompleteFuture.complete(null)
+ } else {
+ onCompleteFuture.completeExceptionally(actualError)
+ }
+ } finally {
+ close()
+ }
+ }
+ }
+ }
+ }
+
+ override fun onCompleteFuture(): CompletableFuture = onCompleteFuture
+
+ override fun close() {
+ val previousState = state.getAndSet(State.CLOSED)
+ if (previousState == State.CLOSED) {
+ return
+ }
+
+ // When the stream is closed, we should always consider it closed. If it closed due
+ // to an error, then we will have already completed the future earlier, and this
+ // will be a no-op.
+ onCompleteFuture.complete(null)
+ }
+}
+
+private enum class State {
+ NEW,
+ SUBSCRIBED,
+ CLOSED,
+}
diff --git a/openai-java-core/src/main/kotlin/com/openai/core/Check.kt b/openai-java-core/src/main/kotlin/com/openai/core/Check.kt
index dc411d75..53b3b919 100644
--- a/openai-java-core/src/main/kotlin/com/openai/core/Check.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/core/Check.kt
@@ -5,6 +5,9 @@ package com.openai.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" }
diff --git a/openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaLocalValidation.kt b/openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaLocalValidation.kt
new file mode 100644
index 00000000..9a3ae799
--- /dev/null
+++ b/openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaLocalValidation.kt
@@ -0,0 +1,19 @@
+package com.openai.core
+
+/**
+ * Options for local validation of JSON schemas derived from arbitrary classes before a request is
+ * executed.
+ */
+enum class JsonSchemaLocalValidation {
+ /**
+ * Validate the JSON schema locally before the request is executed. The remote AI model will
+ * also validate the JSON schema.
+ */
+ YES,
+
+ /**
+ * Do not validate the JSON schema locally before the request is executed. The remote AI model
+ * will always validate the JSON schema.
+ */
+ NO,
+}
diff --git a/openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaValidator.kt b/openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaValidator.kt
new file mode 100644
index 00000000..85c20b43
--- /dev/null
+++ b/openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaValidator.kt
@@ -0,0 +1,668 @@
+package com.openai.core
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.openai.core.JsonSchemaValidator.Companion.MAX_ENUM_TOTAL_STRING_LENGTH
+import com.openai.core.JsonSchemaValidator.Companion.UNRESTRICTED_ENUM_VALUES_LIMIT
+
+/**
+ * A validator that ensures that a JSON schema complies with the rules and restrictions imposed by
+ * the OpenAI API specification for the input schemas used to define structured outputs. Only a
+ * subset of the JSON Schema language is supported. The purpose of this validator is to perform a
+ * quick check of a schema so that it can be determined to be likely to be accepted when passed in
+ * the request for an AI inference.
+ *
+ * This validator assumes that the JSON schema represents the structure of Java/Kotlin classes; it
+ * is not a general-purpose JSON schema validator. Assumptions are also made that the generator will
+ * be well-behaved, so the validation is not a check for strict conformance to the JSON Schema
+ * specification, but to the OpenAI API specification's restrictions on JSON schemas.
+ */
+internal class JsonSchemaValidator private constructor() {
+
+ companion object {
+ // The names of the supported schema keywords. All other keywords will be rejected.
+ private const val SCHEMA = "\$schema"
+ private const val ID = "\$id"
+ private const val DEFS = "\$defs"
+ private const val REF = "\$ref"
+ private const val PROPS = "properties"
+ private const val ANY_OF = "anyOf"
+ private const val TYPE = "type"
+ private const val REQUIRED = "required"
+ private const val DESC = "description"
+ private const val TITLE = "title"
+ private const val ITEMS = "items"
+ private const val CONST = "const"
+ private const val ENUM = "enum"
+ private const val ADDITIONAL_PROPS = "additionalProperties"
+
+ // The names of the supported schema data types.
+ //
+ // JSON Schema does not define an "integer" type, only a "number" type, but it allows any
+ // schema to define its own "vocabulary" of type names. "integer" is supported by OpenAI.
+ private const val TYPE_ARRAY = "array"
+ private const val TYPE_OBJECT = "object"
+ private const val TYPE_BOOLEAN = "boolean"
+ private const val TYPE_STRING = "string"
+ private const val TYPE_NUMBER = "number"
+ private const val TYPE_INTEGER = "integer"
+ private const val TYPE_NULL = "null"
+
+ // The validator checks that unsupported type-specific keywords are not present in a
+ // property node. The OpenAI API specification states:
+ //
+ // "Notable keywords not supported include:
+ //
+ // - For strings: `minLength`, `maxLength`, `pattern`, `format`
+ // - For numbers: `minimum`, `maximum`, `multipleOf`
+ // - For objects: `patternProperties`, `unevaluatedProperties`, `propertyNames`,
+ // `minProperties`, `maxProperties`
+ // - For arrays: `unevaluatedItems`, `contains`, `minContains`, `maxContains`, `minItems`,
+ // `maxItems`, `uniqueItems`"
+ //
+ // As that list is not exhaustive, and no keywords are explicitly named as supported, this
+ // validation allows _no_ type-specific keywords. The following sets define the allowed
+ // keywords in different contexts and all others are rejected.
+
+ /**
+ * The set of allowed keywords in the root schema only, not including the keywords that are
+ * also allowed in a sub-schema.
+ */
+ private val ALLOWED_KEYWORDS_ROOT_SCHEMA_ONLY = setOf(SCHEMA, ID, DEFS)
+
+ /**
+ * The set of allowed keywords when defining sub-schemas when the `"anyOf"` field is
+ * present. OpenAI allows the `"anyOf"` field in sub-schemas, but not in the root schema.
+ */
+ private val ALLOWED_KEYWORDS_ANY_OF_SUB_SCHEMA = setOf(ANY_OF, TITLE, DESC)
+
+ /**
+ * The set of allowed keywords when defining sub-schemas when the `"$ref"` field is present.
+ */
+ private val ALLOWED_KEYWORDS_REF_SUB_SCHEMA = setOf(REF, TITLE, DESC)
+
+ /**
+ * The set of allowed keywords when defining sub-schemas when the `"type"` field is set to
+ * `"object"`.
+ */
+ private val ALLOWED_KEYWORDS_OBJECT_SUB_SCHEMA =
+ setOf(TYPE, REQUIRED, ADDITIONAL_PROPS, TITLE, DESC, PROPS)
+
+ /**
+ * The set of allowed keywords when defining sub-schemas when the `"type"` field is set to
+ * `"array"`.
+ */
+ private val ALLOWED_KEYWORDS_ARRAY_SUB_SCHEMA = setOf(TYPE, TITLE, DESC, ITEMS)
+
+ /**
+ * The set of allowed keywords when defining sub-schemas when the `"type"` field is set to
+ * `"boolean"`, `"integer"`, `"number"`, or `"string"`.
+ */
+ private val ALLOWED_KEYWORDS_SIMPLE_SUB_SCHEMA = setOf(TYPE, TITLE, DESC, ENUM, CONST)
+
+ /**
+ * The maximum total length of all strings used in the schema for property names, definition
+ * names, enum values and const values. The OpenAI specification states:
+ * > In a schema, total string length of all property names, definition names, enum values,
+ * > and const values cannot exceed 15,000 characters.
+ */
+ private const val MAX_TOTAL_STRING_LENGTH = 15_000
+
+ /** The maximum number of object properties allowed in a schema. */
+ private const val MAX_OBJECT_PROPERTIES = 100
+
+ /** The maximum number of enum values across all enums in the schema. */
+ private const val MAX_ENUM_VALUES = 500
+
+ /**
+ * The number of enum values in any one enum with string values beyond which a limit of
+ * [MAX_ENUM_TOTAL_STRING_LENGTH] is imposed on the total length of all the string values of
+ * that one enum.
+ */
+ private const val UNRESTRICTED_ENUM_VALUES_LIMIT = 250
+
+ /**
+ * The maximum total length of all string values of a single enum where the number of values
+ * exceeds [UNRESTRICTED_ENUM_VALUES_LIMIT].
+ */
+ private const val MAX_ENUM_TOTAL_STRING_LENGTH = 7_500
+
+ /** The maximum depth (number of levels) of nesting allowed in a schema. */
+ private const val MAX_NESTING_DEPTH = 5
+
+ /** The depth value that corresponds to the root level of the schema. */
+ private const val ROOT_DEPTH = 0
+
+ /**
+ * The path string that identifies the root node in the schema when appearing in error
+ * messages or references.
+ */
+ private const val ROOT_PATH = "#"
+
+ /**
+ * Creates a new [JsonSchemaValidator]. After calling [validate], the validator instance
+ * holds information about the errors that occurred during validation (if any). A validator
+ * instance can be used only once to validate a schema; to validate another schema, create
+ * another validator.
+ */
+ fun create() = JsonSchemaValidator()
+ }
+
+ /**
+ * The total length of all strings used in the schema for property names, definition names, enum
+ * values and const values.
+ */
+ private var totalStringLength: Int = 0
+
+ /** The total number of values across all enums in the schema. */
+ private var totalEnumValues: Int = 0
+
+ /** The total number of object properties found in the schema, including in definitions. */
+ private var totalObjectProperties: Int = 0
+
+ /**
+ * The set of valid references that may appear in the schema. This set includes the root schema
+ * and any definitions within the root schema. This is used to verify that references elsewhere
+ * in the schema are valid. This will always contain the root schema, but that may be the only
+ * member.
+ */
+ private var validReferences: MutableSet = mutableSetOf(ROOT_PATH)
+
+ /** The list of error messages accumulated during the validation process. */
+ private val errors: MutableList = mutableListOf()
+
+ /**
+ * Indicates if this validator has validated a schema or not. If a schema has been validated,
+ * this validator cannot be used again.
+ */
+ private var isValidationComplete = false
+
+ /**
+ * Gets the list of errors that were recorded during the validation pass.
+ *
+ * @return The list of errors. The list may be empty if no errors were recorded. In that case,
+ * the schema was found to be valid, or has not yet been validated by calling [validate].
+ */
+ fun errors(): List = errors.toImmutable()
+
+ /**
+ * Indicates if a validated schema is valid or not.
+ *
+ * @return `true` if a schema has been validated by calling [validate] and no errors were
+ * reported; or `false` if errors were reported or if a schema has not yet been validated.
+ */
+ fun isValid(): Boolean = isValidationComplete && errors.isEmpty()
+
+ /**
+ * Validates a schema with respect to the OpenAI API specifications.
+ *
+ * @param rootSchema The root node of the tree representing the JSON schema definition.
+ * @return This schema validator for convenience, such as when chaining calls.
+ * @throws IllegalStateException If called a second time. Create a new validator to validate
+ * each new schema.
+ */
+ fun validate(rootSchema: JsonNode): JsonSchemaValidator {
+ check(!isValidationComplete) { "Validation already complete." }
+ isValidationComplete = true
+
+ validateSchema(rootSchema, ROOT_PATH, ROOT_DEPTH)
+
+ // Verify total counts/lengths. These are not localized to a specific element in the schema,
+ // as no one element is the cause of the error; it is the combination of all elements that
+ // exceed the limits. Therefore, the root path is used in the error messages.
+ verify(totalEnumValues <= MAX_ENUM_VALUES, ROOT_PATH) {
+ "Total number of enum values ($totalEnumValues) exceeds limit of $MAX_ENUM_VALUES."
+ }
+ verify(totalStringLength <= MAX_TOTAL_STRING_LENGTH, ROOT_PATH) {
+ "Total string length of all values ($totalStringLength) exceeds " +
+ "limit of $MAX_TOTAL_STRING_LENGTH."
+ }
+ verify(totalObjectProperties <= MAX_OBJECT_PROPERTIES, ROOT_PATH) {
+ "Total number of object properties ($totalObjectProperties) exceeds " +
+ "limit of $MAX_OBJECT_PROPERTIES."
+ }
+
+ return this
+ }
+
+ /**
+ * Validates a schema. This may be the root schema or a sub-schema. Some validations are
+ * specific to the root schema, which is identified by the [depth] being equal to zero.
+ *
+ * This method is recursive: it will validate the given schema and any sub-schemas that it
+ * contains at any depth. References to other schemas (either the root schema or definition
+ * sub-schemas) do not increase the depth of nesting, as those references are not followed
+ * recursively, only checked to be valid internal schema references.
+ *
+ * @param schema The schema to be validated. This may be the root schema or any sub-schema.
+ * @param path The path that identifies the location of this schema within the JSON schema. For
+ * example, for the root schema, this will be `"#"`; for a definition sub-schema of a `Person`
+ * object, this will be `"#/$defs/Person"`.
+ * @param depth The current depth of nesting. The OpenAI API specification places a maximum
+ * limit on the depth of nesting, which will result in an error if it is exceeded. The nesting
+ * depth increases with each recursion into a nested sub-schema. For the root schema, the
+ * nesting depth is zero; all other sub-schemas will have a nesting depth greater than zero.
+ */
+ private fun validateSchema(schema: JsonNode, path: String, depth: Int) {
+ verify(depth <= MAX_NESTING_DEPTH, path) {
+ "Current nesting depth is $depth, but maximum is $MAX_NESTING_DEPTH."
+ }
+
+ verify(schema.isObject, path, { "Schema or sub-schema is not an object." }) {
+ // If the schema is not an object, perform no further validations.
+ return
+ }
+
+ verify(!schema.isEmpty, path) { "Schema or sub-schema is empty." }
+
+ if (depth == ROOT_DEPTH) {
+ // Sanity check for the presence of the "$schema" field, as this makes it more likely
+ // that the schema with `depth == 0` is actually the root node of a JSON schema, not
+ // just a generic JSON document that is being validated in error.
+ verify(schema.get(SCHEMA) != null, path) { "Root schema missing '$SCHEMA' field." }
+ }
+
+ // Before sub-schemas can be validated, the list of definitions must be recorded to ensure
+ // that "$ref" references can be checked for validity. Definitions are optional and only
+ // appear in the root schema.
+ validateDefinitions(schema.get(DEFS), "$path/$DEFS", depth)
+
+ val anyOf = schema.get(ANY_OF)
+ val type = schema.get(TYPE)
+ val ref = schema.get(REF)
+
+ verify(
+ (anyOf != null).xor(type != null).xor(ref != null),
+ path,
+ { "Expected exactly one of '$TYPE' or '$ANY_OF' or '$REF'." },
+ ) {
+ // Validation cannot continue if none are set, or if more than one is set.
+ return
+ }
+
+ validateAnyOfSchema(schema, path, depth)
+ validateTypeSchema(schema, path, depth)
+ validateRefSchema(schema, path, depth)
+ }
+
+ /**
+ * Validates a schema if it has an `"anyOf"` field. OpenAI does not support the use of `"anyOf"`
+ * at the root of a JSON schema. The value is the field is expected to be an array of valid
+ * sub-schemas. If the schema has no `"anyOf"` field, no action is taken.
+ */
+ private fun validateAnyOfSchema(schema: JsonNode, path: String, depth: Int) {
+ val anyOf = schema.get(ANY_OF)
+
+ if (anyOf == null) return
+
+ validateKeywords(schema, ALLOWED_KEYWORDS_ANY_OF_SUB_SCHEMA, path, depth)
+
+ verify(
+ anyOf.isArray && !anyOf.isEmpty,
+ path,
+ { "'$ANY_OF' field is not a non-empty array." },
+ ) {
+ return
+ }
+
+ // Validates that the root schema does not contain an `anyOf` field. This is a restriction
+ // imposed by the OpenAI API specification. `anyOf` fields _can_ appear at other depths.
+ verify(depth != ROOT_DEPTH, path) { "Root schema contains '$ANY_OF' field." }
+
+ // Each entry must be a valid sub-schema.
+ anyOf.forEachIndexed { index, subSchema ->
+ validateSchema(subSchema, "$path/$ANY_OF[$index]", depth + 1)
+ }
+ }
+
+ /**
+ * Validates a schema if it has a `"$ref"` field. The reference is checked to ensure it
+ * corresponds to a valid definition, or is a reference to the root schema. Recursive references
+ * are allowed. If no `"$ref"` field is found in the schema, no action is taken.
+ */
+ private fun validateRefSchema(schema: JsonNode, path: String, depth: Int) {
+ val ref = schema.get(REF)
+
+ if (ref == null) return
+
+ validateKeywords(schema, ALLOWED_KEYWORDS_REF_SUB_SCHEMA, path, depth)
+
+ val refPath = "$path/$REF"
+
+ verify(ref.isTextual, refPath, { "'$REF' field is not a text value." }) {
+ // No point checking the reference has a referent if it is definitely malformed.
+ return
+ }
+ verify(ref.asText() in validReferences, refPath) {
+ "Invalid or unsupported reference: '${ref.asText()}'."
+ }
+ }
+
+ /**
+ * Validates a schema if it has a `"type"` field. This includes most sub-schemas, except those
+ * that have a `"$ref"` or `"anyOf"` field instead. The `"type"` field may be set to a text
+ * value that is the name of the type (e.g., `"object"`, `"array"`, `"number"`), or it may be
+ * set to an array that contains two text values: the name of the type and `"null"`. The OpenAI
+ * API specification explains that this is how a property can be both required (i.e., it must
+ * appear in the JSON document), but its value can be optional (i.e., it can be set explicitly
+ * to `"null"`). If the schema has no `"type"` field, no action is taken.
+ */
+ private fun validateTypeSchema(schema: JsonNode, path: String, depth: Int) {
+ val type = schema.get(TYPE)
+
+ if (type == null) return
+
+ val typeName =
+ if (type.isTextual) {
+ // Type will be something like `"type" : "string"`
+ type.asText()
+ } else if (type.isArray) {
+ // Type will be something like `"type" : [ "string", "null" ]`. This corresponds to
+ // the use of "Optional" in Java/Kotlin.
+ getTypeNameFromTypeArray(type, "$path/$TYPE")
+ } else {
+ error(path) { "'$TYPE' field is not a type name or array of type names." }
+ return
+ }
+
+ when (typeName) {
+ TYPE_ARRAY -> validateArraySchema(schema, path, depth)
+ TYPE_OBJECT -> validateObjectSchema(schema, path, depth)
+
+ TYPE_BOOLEAN,
+ TYPE_INTEGER,
+ TYPE_NUMBER,
+ TYPE_STRING -> validateSimpleSchema(schema, typeName, path, depth)
+
+ // The type name could not be determined from a type name array. An error will already
+ // have been logged by `getTypeNameFromTypeArray`, so no need to do anything more here.
+ null -> return
+
+ else -> error("$path/$TYPE") { "Unsupported '$TYPE' value: '$typeName'." }
+ }
+ }
+
+ /**
+ * Validates a schema whose `"type"` is `"object"`. It is the responsibility of the caller to
+ * ensure that [schema] contains that type definition. If no type, or a different type is
+ * present, the behavior is not defined.
+ */
+ private fun validateObjectSchema(schema: JsonNode, path: String, depth: Int) {
+ validateKeywords(schema, ALLOWED_KEYWORDS_OBJECT_SUB_SCHEMA, path, depth)
+
+ // The schema must declare that additional properties are not allowed. For this check, it
+ // does not matter if there are no "properties" in the schema.
+ verify(
+ schema.get(ADDITIONAL_PROPS) != null &&
+ schema.get(ADDITIONAL_PROPS).asBoolean() == false,
+ path,
+ ) {
+ "'$ADDITIONAL_PROPS' field is missing or is not set to 'false'."
+ }
+
+ val properties = schema.get(PROPS)
+
+ // The "properties" field may be missing (there may be no properties to declare), but if it
+ // is present, it must be a non-empty object, or validation cannot continue.
+ // TODO: Decide if a missing or empty "properties" field is OK or not.
+ verify(
+ properties == null || (properties.isObject && !properties.isEmpty),
+ path,
+ { "'$PROPS' field is not a non-empty object." },
+ ) {
+ return
+ }
+
+ if (properties != null) { // Must be an object.
+ // If a "properties" field is present, there must also be a "required" field. All
+ // properties must be named in the list of required properties.
+ validatePropertiesRequired(
+ properties.fieldNames().asSequence().toSet(),
+ schema.get(REQUIRED),
+ "$path/$REQUIRED",
+ )
+ validateProperties(properties, "$path/$PROPS", depth)
+ }
+ }
+
+ /**
+ * Validates a schema whose `"type"` is `"array"`. It is the responsibility of the caller to
+ * ensure that [schema] contains that type definition. If no type, or a different type is
+ * present, the behavior is not defined.
+ *
+ * An array schema must have an `"items"` field whose value is an object representing a valid
+ * sub-schema.
+ */
+ private fun validateArraySchema(schema: JsonNode, path: String, depth: Int) {
+ validateKeywords(schema, ALLOWED_KEYWORDS_ARRAY_SUB_SCHEMA, path, depth)
+
+ val items = schema.get(ITEMS)
+
+ verify(
+ items != null && items.isObject,
+ path,
+ { "'$ITEMS' field is missing or is not an object." },
+ ) {
+ return
+ }
+
+ validateSchema(items, "$path/$ITEMS", depth + 1)
+ }
+
+ /**
+ * Validates a schema whose `"type"` is one of the supported simple type names other than
+ * `"object"` and `"array"`. It is the responsibility of the caller to ensure that [schema]
+ * contains the correct type definition. If no type, or a different type is present, the
+ * behavior is not defined.
+ *
+ * @param typeName The name of the specific type of the schema. Where the field value is
+ * optional and the type is defined as an array of a type name and a `"null"`, this is the
+ * value of the non-`"null"` type name. For example `"string"`, or `"number"`.
+ */
+ private fun validateSimpleSchema(schema: JsonNode, typeName: String, path: String, depth: Int) {
+ validateKeywords(schema, ALLOWED_KEYWORDS_SIMPLE_SUB_SCHEMA, path, depth)
+
+ val enumField = schema.get(ENUM)
+
+ // OpenAI API specification: "For a single enum property with string values, the total
+ // string length of all enum values cannot exceed 7,500 characters when there are more than
+ // 250 enum values."
+ val isString = typeName == TYPE_STRING
+ var numEnumValues = 0
+ var stringLength = 0
+
+ enumField?.forEach { value ->
+ // OpenAI places limits on the total string length of all enum values across all enums
+ // without being specific about the type of those enums (unlike for enums with string
+ // values, which have their own restrictions noted above). The specification does not
+ // indicate how to count the string length for boolean or number values. Here it is
+ // assumed that their simple string representations should be counted.
+ val length = value.asText().length
+
+ totalStringLength += length
+ totalEnumValues++
+
+ if (isString) {
+ numEnumValues++
+ stringLength += length
+ }
+ }
+
+ verify(
+ !isString ||
+ numEnumValues <= UNRESTRICTED_ENUM_VALUES_LIMIT ||
+ stringLength <= MAX_ENUM_TOTAL_STRING_LENGTH,
+ "$path/$ENUM",
+ ) {
+ "Total string length ($stringLength) of values of an enum with $numEnumValues " +
+ "values exceeds limit of $MAX_ENUM_TOTAL_STRING_LENGTH."
+ }
+
+ schema.get(CONST)?.let { constValue -> totalStringLength += constValue.asText().length }
+ }
+
+ /**
+ * Validates that the definitions (if present) contain fields that each define a valid schema.
+ * Records the names of any definitions to construct the set of possible valid references to
+ * those definitions. This set will be used to validate any references from within definition
+ * sub-schemas, or any other sub-schemas validated at a later time.
+ *
+ * @param defs The node containing the definitions. Definitions are optional, so this node may
+ * be `null`. Definitions may appear in the root schema, but will not appear in any
+ * sub-schemas. If no definitions are present, the list of valid references will not be
+ * changed and no errors will be recorded.
+ * @param path The path that identifies the location within the schema of the `"$defs"` node.
+ * @param depth The current depth of nesting. If definitions are present, this will be zero, as
+ * that is the depth of the root schema.
+ */
+ private fun validateDefinitions(defs: JsonNode?, path: String, depth: Int) {
+ // Definitions are optional. If present, expect an object whose fields are named from the
+ // classes the definitions were extracted from. If not present, do not continue.
+ verify(defs == null || defs.isObject, path, { "'$DEFS' must be an object." }) {
+ return
+ }
+
+ // First, record the valid references to definitions, as any definition sub-schema may
+ // contain a reference to any other definitions sub-schema (including itself) and those
+ // references need to be validated.
+ defs?.fieldNames()?.asSequence()?.forEach { defName ->
+ val reference = "$path/$defName"
+
+ // Consider that there might be duplicate definition names if two different classes
+ // (from different packages) have the same simple name. That would be an error, but
+ // there is no need to stop the validations.
+ // TODO: How should duplicate names be handled? Will the generator use longer names?
+ verify(reference !in validReferences, path) { "Duplicate definition of '$defName'." }
+ validReferences += reference
+ }
+
+ // Second, recursively validate the definition sub-schemas.
+ defs?.fieldNames()?.asSequence()?.forEach { defName ->
+ totalStringLength += defName.length
+ validateSchema(defs.get(defName), "$path/$DEFS/$defName", depth + 1)
+ }
+ }
+
+ /**
+ * Validates that every property in a collection of property names appears in the array of
+ * property names in a `"required"` field.
+ *
+ * @param propertyNames The collection of property names to check in the array of required
+ * properties. This collection will not be empty.
+ * @param required The `"required"` field. This is expected to be a non-`null` array field with
+ * a set of property names.
+ * @param path The path identifying the location of the `"required"` field within the schema.
+ */
+ private fun validatePropertiesRequired(
+ propertyNames: Collection,
+ required: JsonNode?,
+ path: String,
+ ) {
+ val requiredNames = required?.map { it.asText() }?.toSet() ?: emptySet()
+
+ propertyNames.forEach { propertyName ->
+ verify(propertyName in requiredNames, path) {
+ "'$PROPS' field '$propertyName' is not listed as '$REQUIRED'."
+ }
+ }
+ }
+
+ /**
+ * Validates that each named entry in the `"properties"` field of an object schema has a value
+ * that is a valid sub-schema.
+ *
+ * @param properties The `"properties"` field node of an object schema.
+ * @param path The path identifying the location of the `"properties"` field within the schema.
+ */
+ private fun validateProperties(properties: JsonNode, path: String, depth: Int) {
+ val propertyNames = properties.fieldNames().asSequence().toList()
+
+ propertyNames.forEach { propertyName ->
+ totalObjectProperties++
+ totalStringLength += propertyName.length
+ validateSchema(properties.get(propertyName), "$path/$propertyName", depth + 1)
+ }
+ }
+
+ /**
+ * Validates that the names of all fields in the given schema node are present in a collection
+ * of allowed keywords.
+ *
+ * @param depth The nesting depth of the [schema] node. If this depth is zero, an additional set
+ * of allowed keywords will be included automatically for keywords that are allowed to appear
+ * only at the root level of the schema (e.g., `"$schema"`, `"$defs"`).
+ */
+ private fun validateKeywords(
+ schema: JsonNode,
+ allowedKeywords: Collection,
+ path: String,
+ depth: Int,
+ ) {
+ schema.fieldNames().forEach { keyword ->
+ verify(
+ keyword in allowedKeywords ||
+ (depth == ROOT_DEPTH && keyword in ALLOWED_KEYWORDS_ROOT_SCHEMA_ONLY),
+ path,
+ ) {
+ "Use of '$keyword' is not supported here."
+ }
+ }
+ }
+
+ /**
+ * Gets the name of a type from the given `"type"` field, where the field is an array that
+ * contains exactly two string values: a type name and a `"null"` (in any order).
+ *
+ * @param type The type node. This must be a field with an array value. If this is not an array
+ * field, the behavior is undefined. It is the responsibility of the caller to ensure that
+ * this function is only called for array fields.
+ * @return The type name in the array that is not the `"null"` type; or `null` if no such type
+ * name was found, or if the array does not contain exactly two expected values: the type name
+ * and a `"null"` type. If `null`, one or more validation errors will be recorded.
+ */
+ private fun getTypeNameFromTypeArray(type: JsonNode, path: String): String? {
+ val types = type.asSequence().toList()
+
+ if (types.size == 2 && types.all { it.isTextual }) {
+ // Allow one type name and one "null". Be lenient about the order. Assume that there are
+ // no oddities like type names that are empty strings, etc., as the schemas are expected
+ // to be generated.
+ if (types[1].asText() == TYPE_NULL && types[0].asText() != TYPE_NULL) {
+ return types[0].asText()
+ } else if (types[0].asText() == TYPE_NULL && types[1].asText() != TYPE_NULL) {
+ return types[1].asText()
+ } else {
+ error(path) { "Expected one type name and one \"$TYPE_NULL\"." }
+ }
+ } else {
+ error(path) { "Expected exactly two types, both strings." }
+ }
+
+ return null
+ }
+
+ private inline fun verify(value: Boolean, path: String, lazyMessage: () -> Any) {
+ verify(value, path, lazyMessage) {}
+ }
+
+ private inline fun verify(
+ value: Boolean,
+ path: String,
+ lazyMessage: () -> Any,
+ onFalse: () -> Unit,
+ ) {
+ if (!value) {
+ error(path, lazyMessage)
+ onFalse()
+ }
+ }
+
+ private inline fun error(path: String, lazyMessage: () -> Any) {
+ errors.add("$path: ${lazyMessage()}")
+ }
+
+ override fun toString(): String =
+ "${javaClass.simpleName}{isValidationComplete=$isValidationComplete, " +
+ "totalStringLength=$totalStringLength, " +
+ "totalObjectProperties=$totalObjectProperties, " +
+ "totalEnumValues=$totalEnumValues, errors=$errors}"
+}
diff --git a/openai-java-core/src/main/kotlin/com/openai/core/Page.kt b/openai-java-core/src/main/kotlin/com/openai/core/Page.kt
new file mode 100644
index 00000000..9c9560b4
--- /dev/null
+++ b/openai-java-core/src/main/kotlin/com/openai/core/Page.kt
@@ -0,0 +1,33 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.openai.core
+
+/**
+ * An interface representing a single page, with items of type [T], from a paginated endpoint
+ * response.
+ *
+ * Implementations of this interface are expected to request additional pages synchronously. For
+ * asynchronous pagination, see the [PageAsync] interface.
+ */
+interface Page {
+
+ /**
+ * Returns whether there's another page after this one.
+ *
+ * The method generally doesn't make requests so the result depends entirely on the data in this
+ * page. If a significant amount of time has passed between requesting this page and calling
+ * this method, then the result could be stale.
+ */
+ fun hasNextPage(): Boolean
+
+ /**
+ * Returns the page after this one by making another request.
+ *
+ * @throws IllegalStateException if it's impossible to get the next page. This exception is
+ * avoidable by calling [hasNextPage] first.
+ */
+ fun nextPage(): Page
+
+ /** Returns the items in this page. */
+ fun items(): List
+}
diff --git a/openai-java-core/src/main/kotlin/com/openai/core/PageAsync.kt b/openai-java-core/src/main/kotlin/com/openai/core/PageAsync.kt
new file mode 100644
index 00000000..c67ff791
--- /dev/null
+++ b/openai-java-core/src/main/kotlin/com/openai/core/PageAsync.kt
@@ -0,0 +1,35 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.openai.core
+
+import java.util.concurrent.CompletableFuture
+
+/**
+ * An interface representing a single page, with items of type [T], from a paginated endpoint
+ * response.
+ *
+ * Implementations of this interface are expected to request additional pages asynchronously. For
+ * synchronous pagination, see the [Page] interface.
+ */
+interface PageAsync {
+
+ /**
+ * Returns whether there's another page after this one.
+ *
+ * The method generally doesn't make requests so the result depends entirely on the data in this
+ * page. If a significant amount of time has passed between requesting this page and calling
+ * this method, then the result could be stale.
+ */
+ fun hasNextPage(): Boolean
+
+ /**
+ * Returns the page after this one by making another request.
+ *
+ * @throws IllegalStateException if it's impossible to get the next page. This exception is
+ * avoidable by calling [hasNextPage] first.
+ */
+ fun nextPage(): CompletableFuture>
+
+ /** Returns the items in this page. */
+ fun items(): List
+}
diff --git a/openai-java-core/src/main/kotlin/com/openai/core/StructuredOutputs.kt b/openai-java-core/src/main/kotlin/com/openai/core/StructuredOutputs.kt
new file mode 100644
index 00000000..df48c1af
--- /dev/null
+++ b/openai-java-core/src/main/kotlin/com/openai/core/StructuredOutputs.kt
@@ -0,0 +1,132 @@
+package com.openai.core
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.json.JsonMapper
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
+import com.fasterxml.jackson.module.kotlin.kotlinModule
+import com.github.victools.jsonschema.generator.Option
+import com.github.victools.jsonschema.generator.OptionPreset
+import com.github.victools.jsonschema.generator.SchemaGenerator
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder
+import com.github.victools.jsonschema.module.jackson.JacksonModule
+import com.openai.errors.OpenAIInvalidDataException
+import com.openai.models.ResponseFormatJsonSchema
+import com.openai.models.responses.ResponseFormatTextJsonSchemaConfig
+import com.openai.models.responses.ResponseTextConfig
+
+// The SDK `ObjectMappers.jsonMapper()` requires that all fields of classes be marked with
+// `@JsonProperty`, which is not desirable in this context, as it impedes usability. Therefore, a
+// custom JSON mapper configuration is required.
+private val MAPPER =
+ JsonMapper.builder()
+ .addModule(kotlinModule())
+ .addModule(Jdk8Module())
+ .addModule(JavaTimeModule())
+ .build()
+
+/**
+ * Builds a response format using a JSON schema derived from the structure of an arbitrary Java
+ * class.
+ */
+@JvmSynthetic
+internal fun responseFormatFromClass(
+ type: Class,
+ localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES,
+): ResponseFormatJsonSchema =
+ ResponseFormatJsonSchema.builder()
+ .jsonSchema(
+ ResponseFormatJsonSchema.JsonSchema.builder()
+ .name("json-schema-from-${type.simpleName}")
+ .schema(JsonValue.fromJsonNode(extractAndValidateSchema(type, localValidation)))
+ // Ensure the model's output strictly adheres to this JSON schema. This is the
+ // essential "ON switch" for Structured Outputs.
+ .strict(true)
+ .build()
+ )
+ .build()
+
+private fun extractAndValidateSchema(
+ type: Class,
+ localValidation: JsonSchemaLocalValidation,
+): JsonNode {
+ val schema = extractSchema(type)
+
+ if (localValidation == JsonSchemaLocalValidation.YES) {
+ val validator = JsonSchemaValidator.create().validate(schema)
+
+ require(validator.isValid()) {
+ "Local validation failed for JSON schema derived from $type:\n" +
+ validator.errors().joinToString("\n") { " - $it" }
+ }
+ }
+
+ return schema
+}
+
+/**
+ * Builds a text configuration with its format set to a JSON schema derived from the structure of an
+ * arbitrary Java class.
+ */
+@JvmSynthetic
+internal fun textConfigFromClass(
+ type: Class,
+ localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES,
+): ResponseTextConfig =
+ ResponseTextConfig.builder()
+ .format(
+ ResponseFormatTextJsonSchemaConfig.builder()
+ .name("json-schema-from-${type.simpleName}")
+ .schema(JsonValue.fromJsonNode(extractAndValidateSchema(type, localValidation)))
+ // Ensure the model's output strictly adheres to this JSON schema. This is the
+ // essential "ON switch" for Structured Outputs.
+ .strict(true)
+ .build()
+ )
+ .build()
+
+/**
+ * Derives a JSON schema from the structure of an arbitrary Java class.
+ *
+ * Validation is not performed by this function, as it allows extraction of the schema and
+ * validation of the schema to be controlled more easily when unit testing, as no exceptions will be
+ * thrown and any recorded validation errors can be inspected at leisure by the tests.
+ */
+@JvmSynthetic
+internal fun extractSchema(type: Class): JsonNode {
+ val configBuilder =
+ SchemaGeneratorConfigBuilder(
+ com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12,
+ OptionPreset.PLAIN_JSON,
+ )
+ // Add `"additionalProperties" : false` to all object schemas (see OpenAI).
+ .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT)
+ // Use `JacksonModule` to support the use of Jackson annotations to set property and
+ // class names and descriptions and to mark fields with `@JsonIgnore`.
+ .with(JacksonModule())
+
+ configBuilder
+ .forFields()
+ // For OpenAI schemas, _all_ properties _must_ be required. Override the interpretation of
+ // the Jackson `required` parameter to the `@JsonProperty` annotation: it will always be
+ // assumed to be `true`, even if explicitly `false` and even if there is no `@JsonProperty`
+ // annotation present.
+ .withRequiredCheck { true }
+
+ return SchemaGenerator(configBuilder.build()).generateSchema(type)
+}
+
+/**
+ * Creates an instance of a Java class using data from a JSON. The JSON data should conform to the
+ * JSON schema previously extracted from the Java class.
+ */
+@JvmSynthetic
+internal fun responseTypeFromJson(json: String, responseType: Class): T =
+ try {
+ MAPPER.readValue(json, responseType)
+ } catch (e: Exception) {
+ // The JSON document is included in the exception message to aid diagnosis of the problem.
+ // It is the responsibility of the SDK user to ensure that exceptions that may contain
+ // sensitive data are not exposed in logs.
+ throw OpenAIInvalidDataException("Error parsing JSON: $json", e)
+ }
diff --git a/openai-java-core/src/main/kotlin/com/openai/models/ChatModel.kt b/openai-java-core/src/main/kotlin/com/openai/models/ChatModel.kt
index 8546b787..e899ae2a 100644
--- a/openai-java-core/src/main/kotlin/com/openai/models/ChatModel.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/models/ChatModel.kt
@@ -86,6 +86,8 @@ class ChatModel @JsonCreator private constructor(private val value: JsonField Value.GPT_4O_SEARCH_PREVIEW_2025_03_11
GPT_4O_MINI_SEARCH_PREVIEW_2025_03_11 -> Value.GPT_4O_MINI_SEARCH_PREVIEW_2025_03_11
CHATGPT_4O_LATEST -> Value.CHATGPT_4O_LATEST
+ CODEX_MINI_LATEST -> Value.CODEX_MINI_LATEST
GPT_4O_MINI -> Value.GPT_4O_MINI
GPT_4O_MINI_2024_07_18 -> Value.GPT_4O_MINI_2024_07_18
GPT_4_TURBO -> Value.GPT_4_TURBO
@@ -362,6 +367,7 @@ class ChatModel @JsonCreator private constructor(private val value: JsonField Known.GPT_4O_SEARCH_PREVIEW_2025_03_11
GPT_4O_MINI_SEARCH_PREVIEW_2025_03_11 -> Known.GPT_4O_MINI_SEARCH_PREVIEW_2025_03_11
CHATGPT_4O_LATEST -> Known.CHATGPT_4O_LATEST
+ CODEX_MINI_LATEST -> Known.CODEX_MINI_LATEST
GPT_4O_MINI -> Known.GPT_4O_MINI
GPT_4O_MINI_2024_07_18 -> Known.GPT_4O_MINI_2024_07_18
GPT_4_TURBO -> Known.GPT_4_TURBO
diff --git a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionCreateParams.kt b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionCreateParams.kt
index 778b4914..226cf34a 100644
--- a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionCreateParams.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionCreateParams.kt
@@ -2,15 +2,29 @@
package com.openai.models.audio.transcriptions
+import com.fasterxml.jackson.annotation.JsonAnyGetter
+import com.fasterxml.jackson.annotation.JsonAnySetter
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.ObjectCodec
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
+import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
+import com.openai.core.BaseDeserializer
+import com.openai.core.BaseSerializer
import com.openai.core.Enum
import com.openai.core.ExcludeMissing
import com.openai.core.JsonField
+import com.openai.core.JsonValue
import com.openai.core.MultipartField
import com.openai.core.Params
+import com.openai.core.allMaxBy
import com.openai.core.checkKnown
import com.openai.core.checkRequired
+import com.openai.core.getOrThrow
import com.openai.core.http.Headers
import com.openai.core.http.QueryParams
import com.openai.core.toImmutable
@@ -19,10 +33,12 @@ import com.openai.models.audio.AudioModel
import com.openai.models.audio.AudioResponseFormat
import java.io.InputStream
import java.nio.file.Path
+import java.util.Collections
import java.util.Objects
import java.util.Optional
import kotlin.io.path.inputStream
import kotlin.io.path.name
+import kotlin.jvm.optionals.getOrNull
/** Transcribes audio into the input language. */
class TranscriptionCreateParams
@@ -50,6 +66,17 @@ private constructor(
*/
fun model(): AudioModel = body.model()
+ /**
+ * Controls how the audio is cut into chunks. When set to `"auto"`, the server first normalizes
+ * loudness and then uses voice activity detection (VAD) to choose boundaries. `server_vad`
+ * object can be provided to tweak VAD detection parameters manually. If unset, the audio is
+ * transcribed as a single block.
+ *
+ * @throws OpenAIInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun chunkingStrategy(): Optional = body.chunkingStrategy()
+
/**
* Additional information to include in the transcription response. `logprobs` will return the
* log probabilities of the tokens in the response to understand the model's confidence in the
@@ -128,6 +155,14 @@ private constructor(
*/
fun _model(): MultipartField = body._model()
+ /**
+ * Returns the raw multipart value of [chunkingStrategy].
+ *
+ * Unlike [chunkingStrategy], this method doesn't throw if the multipart field has an unexpected
+ * type.
+ */
+ fun _chunkingStrategy(): MultipartField = body._chunkingStrategy()
+
/**
* Returns the raw multipart value of [include].
*
@@ -215,9 +250,9 @@ private constructor(
* Otherwise, it's more convenient to use the top-level setters instead:
* - [file]
* - [model]
+ * - [chunkingStrategy]
* - [include]
* - [language]
- * - [prompt]
* - etc.
*/
fun body(body: Body) = apply { this.body = body.toBuilder() }
@@ -272,6 +307,39 @@ private constructor(
*/
fun model(value: String) = apply { body.model(value) }
+ /**
+ * Controls how the audio is cut into chunks. When set to `"auto"`, the server first
+ * normalizes loudness and then uses voice activity detection (VAD) to choose boundaries.
+ * `server_vad` object can be provided to tweak VAD detection parameters manually. If unset,
+ * the audio is transcribed as a single block.
+ */
+ fun chunkingStrategy(chunkingStrategy: ChunkingStrategy?) = apply {
+ body.chunkingStrategy(chunkingStrategy)
+ }
+
+ /** Alias for calling [Builder.chunkingStrategy] with `chunkingStrategy.orElse(null)`. */
+ fun chunkingStrategy(chunkingStrategy: Optional) =
+ chunkingStrategy(chunkingStrategy.getOrNull())
+
+ /**
+ * Sets [Builder.chunkingStrategy] to an arbitrary multipart value.
+ *
+ * You should usually call [Builder.chunkingStrategy] with a well-typed [ChunkingStrategy]
+ * value instead. This method is primarily for setting the field to an undocumented or not
+ * yet supported value.
+ */
+ fun chunkingStrategy(chunkingStrategy: MultipartField) = apply {
+ body.chunkingStrategy(chunkingStrategy)
+ }
+
+ /** Alias for calling [chunkingStrategy] with `ChunkingStrategy.ofAuto()`. */
+ fun chunkingStrategyAuto() = apply { body.chunkingStrategyAuto() }
+
+ /** Alias for calling [chunkingStrategy] with `ChunkingStrategy.ofVadConfig(vadConfig)`. */
+ fun chunkingStrategy(vadConfig: ChunkingStrategy.VadConfig) = apply {
+ body.chunkingStrategy(vadConfig)
+ }
+
/**
* Additional information to include in the transcription response. `logprobs` will return
* the log probabilities of the tokens in the response to understand the model's confidence
@@ -521,6 +589,7 @@ private constructor(
mapOf(
"file" to _file(),
"model" to _model(),
+ "chunking_strategy" to _chunkingStrategy(),
"include" to _include(),
"language" to _language(),
"prompt" to _prompt(),
@@ -538,6 +607,7 @@ private constructor(
private constructor(
private val file: MultipartField,
private val model: MultipartField,
+ private val chunkingStrategy: MultipartField,
private val include: MultipartField>,
private val language: MultipartField,
private val prompt: MultipartField,
@@ -564,6 +634,18 @@ private constructor(
*/
fun model(): AudioModel = model.value.getRequired("model")
+ /**
+ * Controls how the audio is cut into chunks. When set to `"auto"`, the server first
+ * normalizes loudness and then uses voice activity detection (VAD) to choose boundaries.
+ * `server_vad` object can be provided to tweak VAD detection parameters manually. If unset,
+ * the audio is transcribed as a single block.
+ *
+ * @throws OpenAIInvalidDataException if the JSON field has an unexpected type (e.g. if the
+ * server responded with an unexpected value).
+ */
+ fun chunkingStrategy(): Optional =
+ chunkingStrategy.value.getOptional("chunking_strategy")
+
/**
* Additional information to include in the transcription response. `logprobs` will return
* the log probabilities of the tokens in the response to understand the model's confidence
@@ -644,6 +726,16 @@ private constructor(
*/
@JsonProperty("model") @ExcludeMissing fun _model(): MultipartField = model
+ /**
+ * Returns the raw multipart value of [chunkingStrategy].
+ *
+ * Unlike [chunkingStrategy], this method doesn't throw if the multipart field has an
+ * unexpected type.
+ */
+ @JsonProperty("chunking_strategy")
+ @ExcludeMissing
+ fun _chunkingStrategy(): MultipartField = chunkingStrategy
+
/**
* Returns the raw multipart value of [include].
*
@@ -721,6 +813,7 @@ private constructor(
private var file: MultipartField? = null
private var model: MultipartField? = null
+ private var chunkingStrategy: MultipartField = MultipartField.of(null)
private var include: MultipartField>? = null
private var language: MultipartField = MultipartField.of(null)
private var prompt: MultipartField = MultipartField.of(null)
@@ -734,6 +827,7 @@ private constructor(
internal fun from(body: Body) = apply {
file = body.file
model = body.model
+ chunkingStrategy = body.chunkingStrategy
include = body.include.map { it.toMutableList() }
language = body.language
prompt = body.prompt
@@ -800,6 +894,41 @@ private constructor(
*/
fun model(value: String) = model(AudioModel.of(value))
+ /**
+ * Controls how the audio is cut into chunks. When set to `"auto"`, the server first
+ * normalizes loudness and then uses voice activity detection (VAD) to choose
+ * boundaries. `server_vad` object can be provided to tweak VAD detection parameters
+ * manually. If unset, the audio is transcribed as a single block.
+ */
+ fun chunkingStrategy(chunkingStrategy: ChunkingStrategy?) =
+ chunkingStrategy(MultipartField.of(chunkingStrategy))
+
+ /**
+ * Alias for calling [Builder.chunkingStrategy] with `chunkingStrategy.orElse(null)`.
+ */
+ fun chunkingStrategy(chunkingStrategy: Optional) =
+ chunkingStrategy(chunkingStrategy.getOrNull())
+
+ /**
+ * Sets [Builder.chunkingStrategy] to an arbitrary multipart value.
+ *
+ * You should usually call [Builder.chunkingStrategy] with a well-typed
+ * [ChunkingStrategy] value instead. This method is primarily for setting the field to
+ * an undocumented or not yet supported value.
+ */
+ fun chunkingStrategy(chunkingStrategy: MultipartField) = apply {
+ this.chunkingStrategy = chunkingStrategy
+ }
+
+ /** Alias for calling [chunkingStrategy] with `ChunkingStrategy.ofAuto()`. */
+ fun chunkingStrategyAuto() = chunkingStrategy(ChunkingStrategy.ofAuto())
+
+ /**
+ * Alias for calling [chunkingStrategy] with `ChunkingStrategy.ofVadConfig(vadConfig)`.
+ */
+ fun chunkingStrategy(vadConfig: ChunkingStrategy.VadConfig) =
+ chunkingStrategy(ChunkingStrategy.ofVadConfig(vadConfig))
+
/**
* Additional information to include in the transcription response. `logprobs` will
* return the log probabilities of the tokens in the response to understand the model's
@@ -953,6 +1082,7 @@ private constructor(
Body(
checkRequired("file", file),
checkRequired("model", model),
+ chunkingStrategy,
(include ?: MultipartField.of(null)).map { it.toImmutable() },
language,
prompt,
@@ -971,6 +1101,7 @@ private constructor(
file()
model().validate()
+ chunkingStrategy().ifPresent { it.validate() }
include().ifPresent { it.forEach { it.validate() } }
language()
prompt()
@@ -993,17 +1124,612 @@ private constructor(
return true
}
- return /* spotless:off */ other is Body && file == other.file && model == other.model && include == other.include && language == other.language && prompt == other.prompt && responseFormat == other.responseFormat && temperature == other.temperature && timestampGranularities == other.timestampGranularities /* spotless:on */
+ return /* spotless:off */ other is Body && file == other.file && model == other.model && chunkingStrategy == other.chunkingStrategy && include == other.include && language == other.language && prompt == other.prompt && responseFormat == other.responseFormat && temperature == other.temperature && timestampGranularities == other.timestampGranularities /* spotless:on */
}
/* spotless:off */
- private val hashCode: Int by lazy { Objects.hash(file, model, include, language, prompt, responseFormat, temperature, timestampGranularities) }
+ private val hashCode: Int by lazy { Objects.hash(file, model, chunkingStrategy, include, language, prompt, responseFormat, temperature, timestampGranularities) }
/* spotless:on */
override fun hashCode(): Int = hashCode
override fun toString() =
- "Body{file=$file, model=$model, include=$include, language=$language, prompt=$prompt, responseFormat=$responseFormat, temperature=$temperature, timestampGranularities=$timestampGranularities}"
+ "Body{file=$file, model=$model, chunkingStrategy=$chunkingStrategy, include=$include, language=$language, prompt=$prompt, responseFormat=$responseFormat, temperature=$temperature, timestampGranularities=$timestampGranularities}"
+ }
+
+ /**
+ * Controls how the audio is cut into chunks. When set to `"auto"`, the server first normalizes
+ * loudness and then uses voice activity detection (VAD) to choose boundaries. `server_vad`
+ * object can be provided to tweak VAD detection parameters manually. If unset, the audio is
+ * transcribed as a single block.
+ */
+ @JsonDeserialize(using = ChunkingStrategy.Deserializer::class)
+ @JsonSerialize(using = ChunkingStrategy.Serializer::class)
+ class ChunkingStrategy
+ private constructor(
+ private val auto: JsonValue? = null,
+ private val vadConfig: VadConfig? = null,
+ private val _json: JsonValue? = null,
+ ) {
+
+ /** Automatically set chunking parameters based on the audio. Must be set to `"auto"`. */
+ fun auto(): Optional = Optional.ofNullable(auto)
+
+ fun vadConfig(): Optional = Optional.ofNullable(vadConfig)
+
+ fun isAuto(): Boolean = auto != null
+
+ fun isVadConfig(): Boolean = vadConfig != null
+
+ /** Automatically set chunking parameters based on the audio. Must be set to `"auto"`. */
+ fun asAuto(): JsonValue = auto.getOrThrow("auto")
+
+ fun asVadConfig(): VadConfig = vadConfig.getOrThrow("vadConfig")
+
+ fun _json(): Optional = Optional.ofNullable(_json)
+
+ fun accept(visitor: Visitor): T =
+ when {
+ auto != null -> visitor.visitAuto(auto)
+ vadConfig != null -> visitor.visitVadConfig(vadConfig)
+ else -> visitor.unknown(_json)
+ }
+
+ private var validated: Boolean = false
+
+ fun validate(): ChunkingStrategy = apply {
+ if (validated) {
+ return@apply
+ }
+
+ accept(
+ object : Visitor {
+ override fun visitAuto(auto: JsonValue) {
+ auto.let {
+ if (it != JsonValue.from("auto")) {
+ throw OpenAIInvalidDataException("'auto' is invalid, received $it")
+ }
+ }
+ }
+
+ override fun visitVadConfig(vadConfig: VadConfig) {
+ vadConfig.validate()
+ }
+ }
+ )
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: OpenAIInvalidDataException) {
+ false
+ }
+
+ /**
+ * Returns a score indicating how many valid values are contained in this object
+ * recursively.
+ *
+ * Used for best match union deserialization.
+ */
+ @JvmSynthetic
+ internal fun validity(): Int =
+ accept(
+ object : Visitor {
+ override fun visitAuto(auto: JsonValue) =
+ auto.let { if (it == JsonValue.from("auto")) 1 else 0 }
+
+ override fun visitVadConfig(vadConfig: VadConfig) = 1
+
+ override fun unknown(json: JsonValue?) = 0
+ }
+ )
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return /* spotless:off */ other is ChunkingStrategy && auto == other.auto && vadConfig == other.vadConfig /* spotless:on */
+ }
+
+ override fun hashCode(): Int = /* spotless:off */ Objects.hash(auto, vadConfig) /* spotless:on */
+
+ override fun toString(): String =
+ when {
+ auto != null -> "ChunkingStrategy{auto=$auto}"
+ vadConfig != null -> "ChunkingStrategy{vadConfig=$vadConfig}"
+ _json != null -> "ChunkingStrategy{_unknown=$_json}"
+ else -> throw IllegalStateException("Invalid ChunkingStrategy")
+ }
+
+ companion object {
+
+ /**
+ * Automatically set chunking parameters based on the audio. Must be set to `"auto"`.
+ */
+ @JvmStatic fun ofAuto() = ChunkingStrategy(auto = JsonValue.from("auto"))
+
+ @JvmStatic
+ fun ofVadConfig(vadConfig: VadConfig) = ChunkingStrategy(vadConfig = vadConfig)
+ }
+
+ /**
+ * An interface that defines how to map each variant of [ChunkingStrategy] to a value of
+ * type [T].
+ */
+ interface Visitor {
+
+ /**
+ * Automatically set chunking parameters based on the audio. Must be set to `"auto"`.
+ */
+ fun visitAuto(auto: JsonValue): T
+
+ fun visitVadConfig(vadConfig: VadConfig): T
+
+ /**
+ * Maps an unknown variant of [ChunkingStrategy] to a value of type [T].
+ *
+ * An instance of [ChunkingStrategy] can contain an unknown variant if it was
+ * deserialized from data that doesn't match any known variant. For example, if the SDK
+ * is on an older version than the API, then the API may respond with new variants that
+ * the SDK is unaware of.
+ *
+ * @throws OpenAIInvalidDataException in the default implementation.
+ */
+ fun unknown(json: JsonValue?): T {
+ throw OpenAIInvalidDataException("Unknown ChunkingStrategy: $json")
+ }
+ }
+
+ internal class Deserializer : BaseDeserializer(ChunkingStrategy::class) {
+
+ override fun ObjectCodec.deserialize(node: JsonNode): ChunkingStrategy {
+ val json = JsonValue.fromJsonNode(node)
+
+ val bestMatches =
+ sequenceOf(
+ tryDeserialize(node, jacksonTypeRef())
+ ?.let { ChunkingStrategy(auto = it, _json = json) }
+ ?.takeIf { it.isValid() },
+ tryDeserialize(node, jacksonTypeRef())?.let {
+ ChunkingStrategy(vadConfig = it, _json = json)
+ },
+ )
+ .filterNotNull()
+ .allMaxBy { it.validity() }
+ .toList()
+ return when (bestMatches.size) {
+ // This can happen if what we're deserializing is completely incompatible with
+ // all the possible variants (e.g. deserializing from array).
+ 0 -> ChunkingStrategy(_json = json)
+ 1 -> bestMatches.single()
+ // If there's more than one match with the highest validity, then use the first
+ // completely valid match, or simply the first match if none are completely
+ // valid.
+ else -> bestMatches.firstOrNull { it.isValid() } ?: bestMatches.first()
+ }
+ }
+ }
+
+ internal class Serializer : BaseSerializer(ChunkingStrategy::class) {
+
+ override fun serialize(
+ value: ChunkingStrategy,
+ generator: JsonGenerator,
+ provider: SerializerProvider,
+ ) {
+ when {
+ value.auto != null -> generator.writeObject(value.auto)
+ value.vadConfig != null -> generator.writeObject(value.vadConfig)
+ value._json != null -> generator.writeObject(value._json)
+ else -> throw IllegalStateException("Invalid ChunkingStrategy")
+ }
+ }
+ }
+
+ class VadConfig
+ private constructor(
+ private val type: MultipartField,
+ private val prefixPaddingMs: MultipartField,
+ private val silenceDurationMs: MultipartField,
+ private val threshold: MultipartField,
+ private val additionalProperties: MutableMap,
+ ) {
+
+ /**
+ * Must be set to `server_vad` to enable manual chunking using server side VAD.
+ *
+ * @throws OpenAIInvalidDataException if the JSON field has an unexpected type or is
+ * unexpectedly missing or null (e.g. if the server responded with an unexpected
+ * value).
+ */
+ fun type(): Type = type.value.getRequired("type")
+
+ /**
+ * Amount of audio to include before the VAD detected speech (in milliseconds).
+ *
+ * @throws OpenAIInvalidDataException if the JSON field has an unexpected type (e.g. if
+ * the server responded with an unexpected value).
+ */
+ fun prefixPaddingMs(): Optional =
+ prefixPaddingMs.value.getOptional("prefix_padding_ms")
+
+ /**
+ * Duration of silence to detect speech stop (in milliseconds). With shorter values the
+ * model will respond more quickly, but may jump in on short pauses from the user.
+ *
+ * @throws OpenAIInvalidDataException if the JSON field has an unexpected type (e.g. if
+ * the server responded with an unexpected value).
+ */
+ fun silenceDurationMs(): Optional =
+ silenceDurationMs.value.getOptional("silence_duration_ms")
+
+ /**
+ * Sensitivity threshold (0.0 to 1.0) for voice activity detection. A higher threshold
+ * will require louder audio to activate the model, and thus might perform better in
+ * noisy environments.
+ *
+ * @throws OpenAIInvalidDataException if the JSON field has an unexpected type (e.g. if
+ * the server responded with an unexpected value).
+ */
+ fun threshold(): Optional = threshold.value.getOptional("threshold")
+
+ /**
+ * Returns the raw multipart value of [type].
+ *
+ * Unlike [type], this method doesn't throw if the multipart field has an unexpected
+ * type.
+ */
+ @JsonProperty("type") @ExcludeMissing fun _type(): MultipartField = type
+
+ /**
+ * Returns the raw multipart value of [prefixPaddingMs].
+ *
+ * Unlike [prefixPaddingMs], this method doesn't throw if the multipart field has an
+ * unexpected type.
+ */
+ @JsonProperty("prefix_padding_ms")
+ @ExcludeMissing
+ fun _prefixPaddingMs(): MultipartField = prefixPaddingMs
+
+ /**
+ * Returns the raw multipart value of [silenceDurationMs].
+ *
+ * Unlike [silenceDurationMs], this method doesn't throw if the multipart field has an
+ * unexpected type.
+ */
+ @JsonProperty("silence_duration_ms")
+ @ExcludeMissing
+ fun _silenceDurationMs(): MultipartField = silenceDurationMs
+
+ /**
+ * Returns the raw multipart value of [threshold].
+ *
+ * Unlike [threshold], this method doesn't throw if the multipart field has an
+ * unexpected type.
+ */
+ @JsonProperty("threshold")
+ @ExcludeMissing
+ fun _threshold(): MultipartField = threshold
+
+ @JsonAnySetter
+ private fun putAdditionalProperty(key: String, value: JsonValue) {
+ additionalProperties.put(key, value)
+ }
+
+ @JsonAnyGetter
+ @ExcludeMissing
+ fun _additionalProperties(): Map =
+ Collections.unmodifiableMap(additionalProperties)
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ /**
+ * Returns a mutable builder for constructing an instance of [VadConfig].
+ *
+ * The following fields are required:
+ * ```java
+ * .type()
+ * ```
+ */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [VadConfig]. */
+ class Builder internal constructor() {
+
+ private var type: MultipartField? = null
+ private var prefixPaddingMs: MultipartField = MultipartField.of(null)
+ private var silenceDurationMs: MultipartField = MultipartField.of(null)
+ private var threshold: MultipartField = MultipartField.of(null)
+ private var additionalProperties: MutableMap = mutableMapOf()
+
+ @JvmSynthetic
+ internal fun from(vadConfig: VadConfig) = apply {
+ type = vadConfig.type
+ prefixPaddingMs = vadConfig.prefixPaddingMs
+ silenceDurationMs = vadConfig.silenceDurationMs
+ threshold = vadConfig.threshold
+ additionalProperties = vadConfig.additionalProperties.toMutableMap()
+ }
+
+ /** Must be set to `server_vad` to enable manual chunking using server side VAD. */
+ fun type(type: Type) = type(MultipartField.of(type))
+
+ /**
+ * Sets [Builder.type] to an arbitrary multipart value.
+ *
+ * You should usually call [Builder.type] with a well-typed [Type] value instead.
+ * This method is primarily for setting the field to an undocumented or not yet
+ * supported value.
+ */
+ fun type(type: MultipartField) = apply { this.type = type }
+
+ /** Amount of audio to include before the VAD detected speech (in milliseconds). */
+ fun prefixPaddingMs(prefixPaddingMs: Long) =
+ prefixPaddingMs(MultipartField.of(prefixPaddingMs))
+
+ /**
+ * Sets [Builder.prefixPaddingMs] to an arbitrary multipart value.
+ *
+ * You should usually call [Builder.prefixPaddingMs] with a well-typed [Long] value
+ * instead. This method is primarily for setting the field to an undocumented or not
+ * yet supported value.
+ */
+ fun prefixPaddingMs(prefixPaddingMs: MultipartField) = apply {
+ this.prefixPaddingMs = prefixPaddingMs
+ }
+
+ /**
+ * Duration of silence to detect speech stop (in milliseconds). With shorter values
+ * the model will respond more quickly, but may jump in on short pauses from the
+ * user.
+ */
+ fun silenceDurationMs(silenceDurationMs: Long) =
+ silenceDurationMs(MultipartField.of(silenceDurationMs))
+
+ /**
+ * Sets [Builder.silenceDurationMs] to an arbitrary multipart value.
+ *
+ * You should usually call [Builder.silenceDurationMs] with a well-typed [Long]
+ * value instead. This method is primarily for setting the field to an undocumented
+ * or not yet supported value.
+ */
+ fun silenceDurationMs(silenceDurationMs: MultipartField) = apply {
+ this.silenceDurationMs = silenceDurationMs
+ }
+
+ /**
+ * Sensitivity threshold (0.0 to 1.0) for voice activity detection. A higher
+ * threshold will require louder audio to activate the model, and thus might perform
+ * better in noisy environments.
+ */
+ fun threshold(threshold: Double) = threshold(MultipartField.of(threshold))
+
+ /**
+ * Sets [Builder.threshold] to an arbitrary multipart value.
+ *
+ * You should usually call [Builder.threshold] with a well-typed [Double] value
+ * instead. This method is primarily for setting the field to an undocumented or not
+ * yet supported value.
+ */
+ fun threshold(threshold: MultipartField) = apply {
+ this.threshold = threshold
+ }
+
+ fun additionalProperties(additionalProperties: Map) = apply {
+ this.additionalProperties.clear()
+ putAllAdditionalProperties(additionalProperties)
+ }
+
+ fun putAdditionalProperty(key: String, value: JsonValue) = apply {
+ additionalProperties.put(key, value)
+ }
+
+ fun putAllAdditionalProperties(additionalProperties: Map) =
+ apply {
+ this.additionalProperties.putAll(additionalProperties)
+ }
+
+ fun removeAdditionalProperty(key: String) = apply {
+ additionalProperties.remove(key)
+ }
+
+ fun removeAllAdditionalProperties(keys: Set) = apply {
+ keys.forEach(::removeAdditionalProperty)
+ }
+
+ /**
+ * Returns an immutable instance of [VadConfig].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .type()
+ * ```
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
+ fun build(): VadConfig =
+ VadConfig(
+ checkRequired("type", type),
+ prefixPaddingMs,
+ silenceDurationMs,
+ threshold,
+ additionalProperties.toMutableMap(),
+ )
+ }
+
+ private var validated: Boolean = false
+
+ fun validate(): VadConfig = apply {
+ if (validated) {
+ return@apply
+ }
+
+ type().validate()
+ prefixPaddingMs()
+ silenceDurationMs()
+ threshold()
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: OpenAIInvalidDataException) {
+ false
+ }
+
+ /** Must be set to `server_vad` to enable manual chunking using server side VAD. */
+ class Type @JsonCreator private constructor(private val value: JsonField) :
+ Enum {
+
+ /**
+ * Returns this class instance's raw value.
+ *
+ * This is usually only useful if this instance was deserialized from data that
+ * doesn't match any known member, and you want to know that value. For example, if
+ * the SDK is on an older version than the API, then the API may respond with new
+ * members that the SDK is unaware of.
+ */
+ @com.fasterxml.jackson.annotation.JsonValue fun _value(): JsonField = value
+
+ companion object {
+
+ @JvmField val SERVER_VAD = of("server_vad")
+
+ @JvmStatic fun of(value: String) = Type(JsonField.of(value))
+ }
+
+ /** An enum containing [Type]'s known values. */
+ enum class Known {
+ SERVER_VAD
+ }
+
+ /**
+ * An enum containing [Type]'s known values, as well as an [_UNKNOWN] member.
+ *
+ * An instance of [Type] can contain an unknown value in a couple of cases:
+ * - It was deserialized from data that doesn't match any known member. For example,
+ * if the SDK is on an older version than the API, then the API may respond with
+ * new members that the SDK is unaware of.
+ * - It was constructed with an arbitrary value using the [of] method.
+ */
+ enum class Value {
+ SERVER_VAD,
+ /**
+ * An enum member indicating that [Type] was instantiated with an unknown value.
+ */
+ _UNKNOWN,
+ }
+
+ /**
+ * Returns an enum member corresponding to this class instance's value, or
+ * [Value._UNKNOWN] if the class was instantiated with an unknown value.
+ *
+ * Use the [known] method instead if you're certain the value is always known or if
+ * you want to throw for the unknown case.
+ */
+ fun value(): Value =
+ when (this) {
+ SERVER_VAD -> Value.SERVER_VAD
+ else -> Value._UNKNOWN
+ }
+
+ /**
+ * Returns an enum member corresponding to this class instance's value.
+ *
+ * Use the [value] method instead if you're uncertain the value is always known and
+ * don't want to throw for the unknown case.
+ *
+ * @throws OpenAIInvalidDataException if this class instance's value is a not a
+ * known member.
+ */
+ fun known(): Known =
+ when (this) {
+ SERVER_VAD -> Known.SERVER_VAD
+ else -> throw OpenAIInvalidDataException("Unknown Type: $value")
+ }
+
+ /**
+ * Returns this class instance's primitive wire representation.
+ *
+ * This differs from the [toString] method because that method is primarily for
+ * debugging and generally doesn't throw.
+ *
+ * @throws OpenAIInvalidDataException if this class instance's value does not have
+ * the expected primitive type.
+ */
+ fun asString(): String =
+ _value().asString().orElseThrow {
+ OpenAIInvalidDataException("Value is not a String")
+ }
+
+ private var validated: Boolean = false
+
+ fun validate(): Type = apply {
+ if (validated) {
+ return@apply
+ }
+
+ known()
+ validated = true
+ }
+
+ fun isValid(): Boolean =
+ try {
+ validate()
+ true
+ } catch (e: OpenAIInvalidDataException) {
+ false
+ }
+
+ /**
+ * Returns a score indicating how many valid values are contained in this object
+ * recursively.
+ *
+ * Used for best match union deserialization.
+ */
+ @JvmSynthetic internal fun validity(): Int = if (value() == Value._UNKNOWN) 0 else 1
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return /* spotless:off */ other is Type && value == other.value /* spotless:on */
+ }
+
+ override fun hashCode() = value.hashCode()
+
+ override fun toString() = value.toString()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return /* spotless:off */ other is VadConfig && type == other.type && prefixPaddingMs == other.prefixPaddingMs && silenceDurationMs == other.silenceDurationMs && threshold == other.threshold && additionalProperties == other.additionalProperties /* spotless:on */
+ }
+
+ /* spotless:off */
+ private val hashCode: Int by lazy { Objects.hash(type, prefixPaddingMs, silenceDurationMs, threshold, additionalProperties) }
+ /* spotless:on */
+
+ override fun hashCode(): Int = hashCode
+
+ override fun toString() =
+ "VadConfig{type=$type, prefixPaddingMs=$prefixPaddingMs, silenceDurationMs=$silenceDurationMs, threshold=$threshold, additionalProperties=$additionalProperties}"
+ }
}
class TimestampGranularity
diff --git a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionSegment.kt b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionSegment.kt
index ffd5f56d..c2de7f04 100644
--- a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionSegment.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionSegment.kt
@@ -21,13 +21,13 @@ import kotlin.jvm.optionals.getOrNull
class TranscriptionSegment
private constructor(
private val id: JsonField,
- private val avgLogprob: JsonField,
- private val compressionRatio: JsonField,
- private val end: JsonField,
- private val noSpeechProb: JsonField,
+ private val avgLogprob: JsonField,
+ private val compressionRatio: JsonField,
+ private val end: JsonField,
+ private val noSpeechProb: JsonField,
private val seek: JsonField,
- private val start: JsonField,
- private val temperature: JsonField,
+ private val start: JsonField,
+ private val temperature: JsonField,
private val text: JsonField,
private val tokens: JsonField>,
private val additionalProperties: MutableMap,
@@ -38,19 +38,19 @@ private constructor(
@JsonProperty("id") @ExcludeMissing id: JsonField = JsonMissing.of(),
@JsonProperty("avg_logprob")
@ExcludeMissing
- avgLogprob: JsonField = JsonMissing.of(),
+ avgLogprob: JsonField = JsonMissing.of(),
@JsonProperty("compression_ratio")
@ExcludeMissing
- compressionRatio: JsonField = JsonMissing.of(),
- @JsonProperty("end") @ExcludeMissing end: JsonField = JsonMissing.of(),
+ compressionRatio: JsonField = JsonMissing.of(),
+ @JsonProperty("end") @ExcludeMissing end: JsonField = JsonMissing.of(),
@JsonProperty("no_speech_prob")
@ExcludeMissing
- noSpeechProb: JsonField = JsonMissing.of(),
+ noSpeechProb: JsonField = JsonMissing.of(),
@JsonProperty("seek") @ExcludeMissing seek: JsonField = JsonMissing.of(),
- @JsonProperty("start") @ExcludeMissing start: JsonField = JsonMissing.of(),
+ @JsonProperty("start") @ExcludeMissing start: JsonField = JsonMissing.of(),
@JsonProperty("temperature")
@ExcludeMissing
- temperature: JsonField = JsonMissing.of(),
+ temperature: JsonField = JsonMissing.of(),
@JsonProperty("text") @ExcludeMissing text: JsonField = JsonMissing.of(),
@JsonProperty("tokens") @ExcludeMissing tokens: JsonField> = JsonMissing.of(),
) : this(
@@ -81,7 +81,7 @@ private constructor(
* @throws OpenAIInvalidDataException if the JSON field has an unexpected type or is
* unexpectedly missing or null (e.g. if the server responded with an unexpected value).
*/
- fun avgLogprob(): Double = avgLogprob.getRequired("avg_logprob")
+ fun avgLogprob(): Float = avgLogprob.getRequired("avg_logprob")
/**
* Compression ratio of the segment. If the value is greater than 2.4, consider the compression
@@ -90,7 +90,7 @@ private constructor(
* @throws OpenAIInvalidDataException if the JSON field has an unexpected type or is
* unexpectedly missing or null (e.g. if the server responded with an unexpected value).
*/
- fun compressionRatio(): Double = compressionRatio.getRequired("compression_ratio")
+ fun compressionRatio(): Float = compressionRatio.getRequired("compression_ratio")
/**
* End time of the segment in seconds.
@@ -98,7 +98,7 @@ private constructor(
* @throws OpenAIInvalidDataException if the JSON field has an unexpected type or is
* unexpectedly missing or null (e.g. if the server responded with an unexpected value).
*/
- fun end(): Double = end.getRequired("end")
+ fun end(): Float = end.getRequired("end")
/**
* Probability of no speech in the segment. If the value is higher than 1.0 and the
@@ -107,7 +107,7 @@ private constructor(
* @throws OpenAIInvalidDataException if the JSON field has an unexpected type or is
* unexpectedly missing or null (e.g. if the server responded with an unexpected value).
*/
- fun noSpeechProb(): Double = noSpeechProb.getRequired("no_speech_prob")
+ fun noSpeechProb(): Float = noSpeechProb.getRequired("no_speech_prob")
/**
* Seek offset of the segment.
@@ -123,7 +123,7 @@ private constructor(
* @throws OpenAIInvalidDataException if the JSON field has an unexpected type or is
* unexpectedly missing or null (e.g. if the server responded with an unexpected value).
*/
- fun start(): Double = start.getRequired("start")
+ fun start(): Float = start.getRequired("start")
/**
* Temperature parameter used for generating the segment.
@@ -131,7 +131,7 @@ private constructor(
* @throws OpenAIInvalidDataException if the JSON field has an unexpected type or is
* unexpectedly missing or null (e.g. if the server responded with an unexpected value).
*/
- fun temperature(): Double = temperature.getRequired("temperature")
+ fun temperature(): Float = temperature.getRequired("temperature")
/**
* Text content of the segment.
@@ -161,7 +161,7 @@ private constructor(
*
* Unlike [avgLogprob], this method doesn't throw if the JSON field has an unexpected type.
*/
- @JsonProperty("avg_logprob") @ExcludeMissing fun _avgLogprob(): JsonField = avgLogprob
+ @JsonProperty("avg_logprob") @ExcludeMissing fun _avgLogprob(): JsonField = avgLogprob
/**
* Returns the raw JSON value of [compressionRatio].
@@ -171,14 +171,14 @@ private constructor(
*/
@JsonProperty("compression_ratio")
@ExcludeMissing
- fun _compressionRatio(): JsonField = compressionRatio
+ fun _compressionRatio(): JsonField = compressionRatio
/**
* Returns the raw JSON value of [end].
*
* Unlike [end], this method doesn't throw if the JSON field has an unexpected type.
*/
- @JsonProperty("end") @ExcludeMissing fun _end(): JsonField = end
+ @JsonProperty("end") @ExcludeMissing fun _end(): JsonField = end
/**
* Returns the raw JSON value of [noSpeechProb].
@@ -187,7 +187,7 @@ private constructor(
*/
@JsonProperty("no_speech_prob")
@ExcludeMissing
- fun _noSpeechProb(): JsonField = noSpeechProb
+ fun _noSpeechProb(): JsonField = noSpeechProb
/**
* Returns the raw JSON value of [seek].
@@ -201,14 +201,14 @@ private constructor(
*
* Unlike [start], this method doesn't throw if the JSON field has an unexpected type.
*/
- @JsonProperty("start") @ExcludeMissing fun _start(): JsonField = start
+ @JsonProperty("start") @ExcludeMissing fun _start(): JsonField = start
/**
* Returns the raw JSON value of [temperature].
*
* Unlike [temperature], this method doesn't throw if the JSON field has an unexpected type.
*/
- @JsonProperty("temperature") @ExcludeMissing fun _temperature(): JsonField = temperature
+ @JsonProperty("temperature") @ExcludeMissing fun _temperature(): JsonField = temperature
/**
* Returns the raw JSON value of [text].
@@ -262,13 +262,13 @@ private constructor(
class Builder internal constructor() {
private var id: JsonField? = null
- private var avgLogprob: JsonField? = null
- private var compressionRatio: JsonField? = null
- private var end: JsonField? = null
- private var noSpeechProb: JsonField? = null
+ private var avgLogprob: JsonField? = null
+ private var compressionRatio: JsonField? = null
+ private var end: JsonField? = null
+ private var noSpeechProb: JsonField? = null
private var seek: JsonField? = null
- private var start: JsonField? = null
- private var temperature: JsonField? = null
+ private var start: JsonField? = null
+ private var temperature: JsonField? = null
private var text: JsonField? = null
private var tokens: JsonField>? = null
private var additionalProperties: MutableMap = mutableMapOf()
@@ -303,60 +303,60 @@ private constructor(
* Average logprob of the segment. If the value is lower than -1, consider the logprobs
* failed.
*/
- fun avgLogprob(avgLogprob: Double) = avgLogprob(JsonField.of(avgLogprob))
+ fun avgLogprob(avgLogprob: Float) = avgLogprob(JsonField.of(avgLogprob))
/**
* Sets [Builder.avgLogprob] to an arbitrary JSON value.
*
- * You should usually call [Builder.avgLogprob] with a well-typed [Double] value instead.
+ * You should usually call [Builder.avgLogprob] with a well-typed [Float] value instead.
* This method is primarily for setting the field to an undocumented or not yet supported
* value.
*/
- fun avgLogprob(avgLogprob: JsonField) = apply { this.avgLogprob = avgLogprob }
+ fun avgLogprob(avgLogprob: JsonField) = apply { this.avgLogprob = avgLogprob }
/**
* Compression ratio of the segment. If the value is greater than 2.4, consider the
* compression failed.
*/
- fun compressionRatio(compressionRatio: Double) =
+ fun compressionRatio(compressionRatio: Float) =
compressionRatio(JsonField.of(compressionRatio))
/**
* Sets [Builder.compressionRatio] to an arbitrary JSON value.
*
- * You should usually call [Builder.compressionRatio] with a well-typed [Double] value
+ * You should usually call [Builder.compressionRatio] with a well-typed [Float] value
* instead. This method is primarily for setting the field to an undocumented or not yet
* supported value.
*/
- fun compressionRatio(compressionRatio: JsonField) = apply {
+ fun compressionRatio(compressionRatio: JsonField) = apply {
this.compressionRatio = compressionRatio
}
/** End time of the segment in seconds. */
- fun end(end: Double) = end(JsonField.of(end))
+ fun end(end: Float) = end(JsonField.of(end))
/**
* Sets [Builder.end] to an arbitrary JSON value.
*
- * You should usually call [Builder.end] with a well-typed [Double] value instead. This
+ * You should usually call [Builder.end] with a well-typed [Float] value instead. This
* method is primarily for setting the field to an undocumented or not yet supported value.
*/
- fun end(end: JsonField) = apply { this.end = end }
+ fun end(end: JsonField) = apply { this.end = end }
/**
* Probability of no speech in the segment. If the value is higher than 1.0 and the
* `avg_logprob` is below -1, consider this segment silent.
*/
- fun noSpeechProb(noSpeechProb: Double) = noSpeechProb(JsonField.of(noSpeechProb))
+ fun noSpeechProb(noSpeechProb: Float) = noSpeechProb(JsonField.of(noSpeechProb))
/**
* Sets [Builder.noSpeechProb] to an arbitrary JSON value.
*
- * You should usually call [Builder.noSpeechProb] with a well-typed [Double] value instead.
+ * You should usually call [Builder.noSpeechProb] with a well-typed [Float] value instead.
* This method is primarily for setting the field to an undocumented or not yet supported
* value.
*/
- fun noSpeechProb(noSpeechProb: JsonField) = apply {
+ fun noSpeechProb(noSpeechProb: JsonField) = apply {
this.noSpeechProb = noSpeechProb
}
@@ -372,27 +372,27 @@ private constructor(
fun seek(seek: JsonField) = apply { this.seek = seek }
/** Start time of the segment in seconds. */
- fun start(start: Double) = start(JsonField.of(start))
+ fun start(start: Float) = start(JsonField.of(start))
/**
* Sets [Builder.start] to an arbitrary JSON value.
*
- * You should usually call [Builder.start] with a well-typed [Double] value instead. This
+ * You should usually call [Builder.start] with a well-typed [Float] value instead. This
* method is primarily for setting the field to an undocumented or not yet supported value.
*/
- fun start(start: JsonField) = apply { this.start = start }
+ fun start(start: JsonField) = apply { this.start = start }
/** Temperature parameter used for generating the segment. */
- fun temperature(temperature: Double) = temperature(JsonField.of(temperature))
+ fun temperature(temperature: Float) = temperature(JsonField.of(temperature))
/**
* Sets [Builder.temperature] to an arbitrary JSON value.
*
- * You should usually call [Builder.temperature] with a well-typed [Double] value instead.
+ * You should usually call [Builder.temperature] with a well-typed [Float] value instead.
* This method is primarily for setting the field to an undocumented or not yet supported
* value.
*/
- fun temperature(temperature: JsonField) = apply { this.temperature = temperature }
+ fun temperature(temperature: JsonField) = apply { this.temperature = temperature }
/** Text content of the segment. */
fun text(text: String) = text(JsonField.of(text))
diff --git a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionWord.kt b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionWord.kt
index e17f2505..2ebd3c15 100644
--- a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionWord.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionWord.kt
@@ -17,16 +17,16 @@ import java.util.Objects
class TranscriptionWord
private constructor(
- private val end: JsonField,
- private val start: JsonField,
+ private val end: JsonField,
+ private val start: JsonField,
private val word: JsonField,
private val additionalProperties: MutableMap,
) {
@JsonCreator
private constructor(
- @JsonProperty("end") @ExcludeMissing end: JsonField = JsonMissing.of(),
- @JsonProperty("start") @ExcludeMissing start: JsonField = JsonMissing.of(),
+ @JsonProperty("end") @ExcludeMissing end: JsonField = JsonMissing.of(),
+ @JsonProperty("start") @ExcludeMissing start: JsonField = JsonMissing.of(),
@JsonProperty("word") @ExcludeMissing word: JsonField = JsonMissing.of(),
) : this(end, start, word, mutableMapOf())
@@ -36,7 +36,7 @@ private constructor(
* @throws OpenAIInvalidDataException if the JSON field has an unexpected type or is
* unexpectedly missing or null (e.g. if the server responded with an unexpected value).
*/
- fun end(): Double = end.getRequired("end")
+ fun end(): Float = end.getRequired("end")
/**
* Start time of the word in seconds.
@@ -44,7 +44,7 @@ private constructor(
* @throws OpenAIInvalidDataException if the JSON field has an unexpected type or is
* unexpectedly missing or null (e.g. if the server responded with an unexpected value).
*/
- fun start(): Double = start.getRequired("start")
+ fun start(): Float = start.getRequired("start")
/**
* The text content of the word.
@@ -59,14 +59,14 @@ private constructor(
*
* Unlike [end], this method doesn't throw if the JSON field has an unexpected type.
*/
- @JsonProperty("end") @ExcludeMissing fun _end(): JsonField = end
+ @JsonProperty("end") @ExcludeMissing fun _end(): JsonField = end
/**
* Returns the raw JSON value of [start].
*
* Unlike [start], this method doesn't throw if the JSON field has an unexpected type.
*/
- @JsonProperty("start") @ExcludeMissing fun _start(): JsonField = start
+ @JsonProperty("start") @ExcludeMissing fun _start(): JsonField = start
/**
* Returns the raw JSON value of [word].
@@ -105,8 +105,8 @@ private constructor(
/** A builder for [TranscriptionWord]. */
class Builder internal constructor() {
- private var end: JsonField? = null
- private var start: JsonField? = null
+ private var end: JsonField? = null
+ private var start: JsonField? = null
private var word: JsonField? = null
private var additionalProperties: MutableMap = mutableMapOf()
@@ -119,26 +119,26 @@ private constructor(
}
/** End time of the word in seconds. */
- fun end(end: Double) = end(JsonField.of(end))
+ fun end(end: Float) = end(JsonField.of(end))
/**
* Sets [Builder.end] to an arbitrary JSON value.
*
- * You should usually call [Builder.end] with a well-typed [Double] value instead. This
+ * You should usually call [Builder.end] with a well-typed [Float] value instead. This
* method is primarily for setting the field to an undocumented or not yet supported value.
*/
- fun end(end: JsonField) = apply { this.end = end }
+ fun end(end: JsonField) = apply { this.end = end }
/** Start time of the word in seconds. */
- fun start(start: Double) = start(JsonField.of(start))
+ fun start(start: Float) = start(JsonField.of(start))
/**
* Sets [Builder.start] to an arbitrary JSON value.
*
- * You should usually call [Builder.start] with a well-typed [Double] value instead. This
+ * You should usually call [Builder.start] with a well-typed [Float] value instead. This
* method is primarily for setting the field to an undocumented or not yet supported value.
*/
- fun start(start: JsonField) = apply { this.start = start }
+ fun start(start: JsonField) = apply { this.start = start }
/** The text content of the word. */
fun word(word: String) = word(JsonField.of(word))
diff --git a/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchCancelParams.kt b/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchCancelParams.kt
index c9a0e725..4fd0d88f 100644
--- a/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchCancelParams.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchCancelParams.kt
@@ -4,12 +4,12 @@ package com.openai.models.batches
import com.openai.core.JsonValue
import com.openai.core.Params
-import com.openai.core.checkRequired
import com.openai.core.http.Headers
import com.openai.core.http.QueryParams
import com.openai.core.toImmutable
import java.util.Objects
import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
/**
* Cancels an in-progress batch. The batch will be in status `cancelling` for up to 10 minutes,
@@ -18,13 +18,13 @@ import java.util.Optional
*/
class BatchCancelParams
private constructor(
- private val batchId: String,
+ private val batchId: String?,
private val additionalHeaders: Headers,
private val additionalQueryParams: QueryParams,
private val additionalBodyProperties: Map,
) : Params {
- fun batchId(): String = batchId
+ fun batchId(): Optional = Optional.ofNullable(batchId)
fun _additionalBodyProperties(): Map = additionalBodyProperties
@@ -36,14 +36,9 @@ private constructor(
companion object {
- /**
- * Returns a mutable builder for constructing an instance of [BatchCancelParams].
- *
- * The following fields are required:
- * ```java
- * .batchId()
- * ```
- */
+ @JvmStatic fun none(): BatchCancelParams = builder().build()
+
+ /** Returns a mutable builder for constructing an instance of [BatchCancelParams]. */
@JvmStatic fun builder() = Builder()
}
@@ -63,7 +58,10 @@ private constructor(
additionalBodyProperties = batchCancelParams.additionalBodyProperties.toMutableMap()
}
- fun batchId(batchId: String) = apply { this.batchId = batchId }
+ fun batchId(batchId: String?) = apply { this.batchId = batchId }
+
+ /** Alias for calling [Builder.batchId] with `batchId.orElse(null)`. */
+ fun batchId(batchId: Optional) = batchId(batchId.getOrNull())
fun additionalHeaders(additionalHeaders: Headers) = apply {
this.additionalHeaders.clear()
@@ -189,17 +187,10 @@ private constructor(
* Returns an immutable instance of [BatchCancelParams].
*
* Further updates to this [Builder] will not mutate the returned instance.
- *
- * The following fields are required:
- * ```java
- * .batchId()
- * ```
- *
- * @throws IllegalStateException if any required field is unset.
*/
fun build(): BatchCancelParams =
BatchCancelParams(
- checkRequired("batchId", batchId),
+ batchId,
additionalHeaders.build(),
additionalQueryParams.build(),
additionalBodyProperties.toImmutable(),
@@ -211,7 +202,7 @@ private constructor(
fun _pathParam(index: Int): String =
when (index) {
- 0 -> batchId
+ 0 -> batchId ?: ""
else -> ""
}
diff --git a/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchListPage.kt b/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchListPage.kt
index 35c7aa3f..d591ebcc 100644
--- a/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchListPage.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchListPage.kt
@@ -2,12 +2,12 @@
package com.openai.models.batches
+import com.openai.core.AutoPager
+import com.openai.core.Page
import com.openai.core.checkRequired
import com.openai.services.blocking.BatchService
import java.util.Objects
import java.util.Optional
-import java.util.stream.Stream
-import java.util.stream.StreamSupport
import kotlin.jvm.optionals.getOrNull
/** @see [BatchService.list] */
@@ -16,7 +16,7 @@ private constructor(
private val service: BatchService,
private val params: BatchListParams,
private val response: BatchListPageResponse,
-) {
+) : Page {
/**
* Delegates to [BatchListPageResponse], but gracefully handles missing data.
@@ -32,19 +32,16 @@ private constructor(
*/
fun hasMore(): Optional = response._hasMore().getOptional("has_more")
- fun hasNextPage(): Boolean = data().isNotEmpty()
+ override fun items(): List = data()
- fun getNextPageParams(): Optional {
- if (!hasNextPage()) {
- return Optional.empty()
- }
+ override fun hasNextPage(): Boolean = items().isNotEmpty()
- return Optional.of(params.toBuilder().after(data().last()._id().getOptional("id")).build())
- }
+ fun nextPageParams(): BatchListParams =
+ params.toBuilder().after(items().last()._id().getOptional("id")).build()
- fun getNextPage(): Optional = getNextPageParams().map { service.list(it) }
+ override fun nextPage(): BatchListPage = service.list(nextPageParams())
- fun autoPager(): AutoPager = AutoPager(this)
+ fun autoPager(): AutoPager = AutoPager.from(this)
/** The parameters that were used to request this page. */
fun params(): BatchListParams = params
@@ -113,25 +110,6 @@ private constructor(
)
}
- class AutoPager(private val firstPage: BatchListPage) : Iterable {
-
- override fun iterator(): Iterator = iterator {
- var page = firstPage
- var index = 0
- while (true) {
- while (index < page.data().size) {
- yield(page.data()[index++])
- }
- page = page.getNextPage().getOrNull() ?: break
- index = 0
- }
- }
-
- fun stream(): Stream {
- return StreamSupport.stream(spliterator(), false)
- }
- }
-
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
diff --git a/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchListPageAsync.kt b/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchListPageAsync.kt
index 61612712..0b36c622 100644
--- a/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchListPageAsync.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchListPageAsync.kt
@@ -2,22 +2,24 @@
package com.openai.models.batches
+import com.openai.core.AutoPagerAsync
+import com.openai.core.PageAsync
import com.openai.core.checkRequired
import com.openai.services.async.BatchServiceAsync
import java.util.Objects
import java.util.Optional
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
-import java.util.function.Predicate
import kotlin.jvm.optionals.getOrNull
/** @see [BatchServiceAsync.list] */
class BatchListPageAsync
private constructor(
private val service: BatchServiceAsync,
+ private val streamHandlerExecutor: Executor,
private val params: BatchListParams,
private val response: BatchListPageResponse,
-) {
+) : PageAsync {
/**
* Delegates to [BatchListPageResponse], but gracefully handles missing data.
@@ -33,22 +35,16 @@ private constructor(
*/
fun hasMore(): Optional = response._hasMore().getOptional("has_more")
- fun hasNextPage(): Boolean = data().isNotEmpty()
+ override fun items(): List = data()
- fun getNextPageParams(): Optional {
- if (!hasNextPage()) {
- return Optional.empty()
- }
+ override fun hasNextPage(): Boolean = items().isNotEmpty()
- return Optional.of(params.toBuilder().after(data().last()._id().getOptional("id")).build())
- }
+ fun nextPageParams(): BatchListParams =
+ params.toBuilder().after(items().last()._id().getOptional("id")).build()
- fun getNextPage(): CompletableFuture> =
- getNextPageParams()
- .map { service.list(it).thenApply { Optional.of(it) } }
- .orElseGet { CompletableFuture.completedFuture(Optional.empty()) }
+ override fun nextPage(): CompletableFuture = service.list(nextPageParams())
- fun autoPager(): AutoPager = AutoPager(this)
+ fun autoPager(): AutoPagerAsync = AutoPagerAsync.from(this, streamHandlerExecutor)
/** The parameters that were used to request this page. */
fun params(): BatchListParams = params
@@ -66,6 +62,7 @@ private constructor(
* The following fields are required:
* ```java
* .service()
+ * .streamHandlerExecutor()
* .params()
* .response()
* ```
@@ -77,18 +74,24 @@ private constructor(
class Builder internal constructor() {
private var service: BatchServiceAsync? = null
+ private var streamHandlerExecutor: Executor? = null
private var params: BatchListParams? = null
private var response: BatchListPageResponse? = null
@JvmSynthetic
internal fun from(batchListPageAsync: BatchListPageAsync) = apply {
service = batchListPageAsync.service
+ streamHandlerExecutor = batchListPageAsync.streamHandlerExecutor
params = batchListPageAsync.params
response = batchListPageAsync.response
}
fun service(service: BatchServiceAsync) = apply { this.service = service }
+ fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply {
+ this.streamHandlerExecutor = streamHandlerExecutor
+ }
+
/** The parameters that were used to request this page. */
fun params(params: BatchListParams) = apply { this.params = params }
@@ -103,6 +106,7 @@ private constructor(
* The following fields are required:
* ```java
* .service()
+ * .streamHandlerExecutor()
* .params()
* .response()
* ```
@@ -112,47 +116,22 @@ private constructor(
fun build(): BatchListPageAsync =
BatchListPageAsync(
checkRequired("service", service),
+ checkRequired("streamHandlerExecutor", streamHandlerExecutor),
checkRequired("params", params),
checkRequired("response", response),
)
}
- class AutoPager(private val firstPage: BatchListPageAsync) {
-
- fun forEach(action: Predicate, executor: Executor): CompletableFuture {
- fun CompletableFuture>.forEach(
- action: (Batch) -> Boolean,
- executor: Executor,
- ): CompletableFuture =
- thenComposeAsync(
- { page ->
- page
- .filter { it.data().all(action) }
- .map { it.getNextPage().forEach(action, executor) }
- .orElseGet { CompletableFuture.completedFuture(null) }
- },
- executor,
- )
- return CompletableFuture.completedFuture(Optional.of(firstPage))
- .forEach(action::test, executor)
- }
-
- fun toList(executor: Executor): CompletableFuture> {
- val values = mutableListOf()
- return forEach(values::add, executor).thenApply { values }
- }
- }
-
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
- return /* spotless:off */ other is BatchListPageAsync && service == other.service && params == other.params && response == other.response /* spotless:on */
+ return /* spotless:off */ other is BatchListPageAsync && service == other.service && streamHandlerExecutor == other.streamHandlerExecutor && params == other.params && response == other.response /* spotless:on */
}
- override fun hashCode(): Int = /* spotless:off */ Objects.hash(service, params, response) /* spotless:on */
+ override fun hashCode(): Int = /* spotless:off */ Objects.hash(service, streamHandlerExecutor, params, response) /* spotless:on */
override fun toString() =
- "BatchListPageAsync{service=$service, params=$params, response=$response}"
+ "BatchListPageAsync{service=$service, streamHandlerExecutor=$streamHandlerExecutor, params=$params, response=$response}"
}
diff --git a/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchRetrieveParams.kt b/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchRetrieveParams.kt
index 94571a8a..d1667cc1 100644
--- a/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchRetrieveParams.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/models/batches/BatchRetrieveParams.kt
@@ -3,20 +3,21 @@
package com.openai.models.batches
import com.openai.core.Params
-import com.openai.core.checkRequired
import com.openai.core.http.Headers
import com.openai.core.http.QueryParams
import java.util.Objects
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
/** Retrieves a batch. */
class BatchRetrieveParams
private constructor(
- private val batchId: String,
+ private val batchId: String?,
private val additionalHeaders: Headers,
private val additionalQueryParams: QueryParams,
) : Params {
- fun batchId(): String = batchId
+ fun batchId(): Optional = Optional.ofNullable(batchId)
fun _additionalHeaders(): Headers = additionalHeaders
@@ -26,14 +27,9 @@ private constructor(
companion object {
- /**
- * Returns a mutable builder for constructing an instance of [BatchRetrieveParams].
- *
- * The following fields are required:
- * ```java
- * .batchId()
- * ```
- */
+ @JvmStatic fun none(): BatchRetrieveParams = builder().build()
+
+ /** Returns a mutable builder for constructing an instance of [BatchRetrieveParams]. */
@JvmStatic fun builder() = Builder()
}
@@ -51,7 +47,10 @@ private constructor(
additionalQueryParams = batchRetrieveParams.additionalQueryParams.toBuilder()
}
- fun batchId(batchId: String) = apply { this.batchId = batchId }
+ fun batchId(batchId: String?) = apply { this.batchId = batchId }
+
+ /** Alias for calling [Builder.batchId] with `batchId.orElse(null)`. */
+ fun batchId(batchId: Optional) = batchId(batchId.getOrNull())
fun additionalHeaders(additionalHeaders: Headers) = apply {
this.additionalHeaders.clear()
@@ -155,25 +154,14 @@ private constructor(
* Returns an immutable instance of [BatchRetrieveParams].
*
* Further updates to this [Builder] will not mutate the returned instance.
- *
- * The following fields are required:
- * ```java
- * .batchId()
- * ```
- *
- * @throws IllegalStateException if any required field is unset.
*/
fun build(): BatchRetrieveParams =
- BatchRetrieveParams(
- checkRequired("batchId", batchId),
- additionalHeaders.build(),
- additionalQueryParams.build(),
- )
+ BatchRetrieveParams(batchId, additionalHeaders.build(), additionalQueryParams.build())
}
fun _pathParam(index: Int): String =
when (index) {
- 0 -> batchId
+ 0 -> batchId ?: ""
else -> ""
}
diff --git a/openai-java-core/src/main/kotlin/com/openai/models/beta/assistants/AssistantDeleteParams.kt b/openai-java-core/src/main/kotlin/com/openai/models/beta/assistants/AssistantDeleteParams.kt
index f0c6d40f..b5df03cb 100644
--- a/openai-java-core/src/main/kotlin/com/openai/models/beta/assistants/AssistantDeleteParams.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/models/beta/assistants/AssistantDeleteParams.kt
@@ -4,23 +4,23 @@ package com.openai.models.beta.assistants
import com.openai.core.JsonValue
import com.openai.core.Params
-import com.openai.core.checkRequired
import com.openai.core.http.Headers
import com.openai.core.http.QueryParams
import com.openai.core.toImmutable
import java.util.Objects
import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
/** Delete an assistant. */
class AssistantDeleteParams
private constructor(
- private val assistantId: String,
+ private val assistantId: String?,
private val additionalHeaders: Headers,
private val additionalQueryParams: QueryParams,
private val additionalBodyProperties: Map,
) : Params {
- fun assistantId(): String = assistantId
+ fun assistantId(): Optional