Skip to content

release: 2.12.0 #518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/openai-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork

steps:
- uses: actions/checkout@v4
Expand All @@ -39,6 +40,7 @@ jobs:
timeout-minutes: 10
name: test
runs-on: ${{ github.repository == 'stainless-sdks/openai-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4

Expand All @@ -60,7 +62,7 @@ jobs:
timeout-minutes: 10
name: examples
runs-on: ${{ github.repository == 'stainless-sdks/openai-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.repository == 'openai/openai-java'
if: github.repository == 'openai/openai-java' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork)

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "2.11.0"
".": "2.12.0"
}
2 changes: 1 addition & 1 deletion .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 88
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-a473967d1766dc155994d932fbc4a5bcbd1c140a37c20d0a4065e1bf0640536d.yml
openapi_spec_hash: 67cdc62b0d6c8b1de29b7dc54b265749
config_hash: 05c7d4a6f4d5983fe9550457114b47dd
config_hash: 7b53f96f897ca1b3407a5341a6f820db
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
# Changelog

## 2.12.0 (2025-07-01)

Full Changelog: [v2.11.0...v2.12.0](https://github.com/openai/openai-java/compare/v2.11.0...v2.12.0)

### Features

* support new schema constraints for structured outputs ([#520](https://github.com/openai/openai-java/issues/520)) ([5c41ac5](https://github.com/openai/openai-java/commit/5c41ac5f1c8ed986e887e06adc3da73ec7e6b5e5))


### Bug Fixes

* **ci:** correct conditional ([a8c7a16](https://github.com/openai/openai-java/commit/a8c7a16184376d0dcfc8dd6f954c303c02888b40))
* **client:** don't close client on `withOptions` usage when original is gc'd ([e0890e3](https://github.com/openai/openai-java/commit/e0890e398aef9a8b6c14aba23b0b2a3d802ced8f))


### Chores

* **ci:** only run for pushes and fork pull requests ([8dc0179](https://github.com/openai/openai-java/commit/8dc0179eeb96772c73e035668904201a86e245c5))


### Documentation

* fix readme typoe ([#521](https://github.com/openai/openai-java/issues/521)) ([eb83a83](https://github.com/openai/openai-java/commit/eb83a83d5b32376497dbad8b74b15347a69ce1dd))


### Refactors

* **internal:** minor `ClientOptionsTest` change ([a7379a2](https://github.com/openai/openai-java/commit/a7379a239d4f93fe631224df81787fdec08d14bd))

## 2.11.0 (2025-06-27)

Full Changelog: [v2.10.0...v2.11.0](https://github.com/openai/openai-java/compare/v2.10.0...v2.11.0)
Expand Down
57 changes: 52 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

<!-- x-release-please-start-version -->

[![Maven Central](https://img.shields.io/maven-central/v/com.openai/openai-java)](https://central.sonatype.com/artifact/com.openai/openai-java/2.11.0)
[![javadoc](https://javadoc.io/badge2/com.openai/openai-java/2.11.0/javadoc.svg)](https://javadoc.io/doc/com.openai/openai-java/2.11.0)
[![Maven Central](https://img.shields.io/maven-central/v/com.openai/openai-java)](https://central.sonatype.com/artifact/com.openai/openai-java/2.12.0)
[![javadoc](https://javadoc.io/badge2/com.openai/openai-java/2.12.0/javadoc.svg)](https://javadoc.io/doc/com.openai/openai-java/2.12.0)

<!-- x-release-please-end -->

The OpenAI Java SDK provides convenient access to the [OpenAI REST API](https://platform.openai.com/docs) from applications written in Java.

<!-- x-release-please-start-version -->

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.11.0).
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.12.0).

<!-- x-release-please-end -->

Expand All @@ -22,7 +22,7 @@ The REST API documentation can be found on [platform.openai.com](https://platfor
### Gradle

```kotlin
implementation("com.openai:openai-java:2.11.0")
implementation("com.openai:openai-java:2.12.0")
```

### Maven
Expand All @@ -31,7 +31,7 @@ implementation("com.openai:openai-java:2.11.0")
<dependency>
<groupId>com.openai</groupId>
<artifactId>openai-java</artifactId>
<version>2.11.0</version>
<version>2.12.0</version>
</dependency>
```

Expand Down Expand Up @@ -580,6 +580,53 @@ If you use `@JsonProperty(required = false)`, the `false` value will be ignored.
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.

You can also use [OpenAPI Swagger 2](https://swagger.io/specification/v2/)
[`@Schema`](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Annotations#schema) and
[`@ArraySchema`](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Annotations#arrayschema)
annotations. These allow type-specific constraints to be added to your schema properties. You can
learn more about the supported constraints in the OpenAI documentation on
[Supported properties](https://platform.openai.com/docs/guides/structured-outputs#supported-properties).

```java
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.ArraySchema;

class Article {
@ArraySchema(minItems = 1, maxItems = 10)
public List<String> authors;

@Schema(pattern = "^[A-Za-z ]+$")
public String title;

@Schema(format = "date")
public String publicationDate;

@Schema(minimum = "1")
public int pageCount;
}
```

Local validation will check that you have not used any unsupported constraint keywords. However, the
values of the constraints are _not_ validated locally. For example, if you use a value for the
`"format"` constraint of a string property that is not in the list of
[supported format names](https://platform.openai.com/docs/guides/structured-outputs#supported-properties),
then local validation will pass, but the AI model may report an error.

If you use both Jackson and Swagger annotations to set the same schema field, the Jackson annotation
will take precedence. In the following example, the description of `myProperty` will be set to
"Jackson description"; "Swagger description" will be ignored:

```java
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.swagger.v3.oas.annotations.media.Schema;

class MyObject {
@Schema(description = "Swagger description")
@JsonPropertyDescription("Jackson description")
public String myProperty;
}
```

## Function calling with JSON schemas

OpenAI [Function Calling](https://platform.openai.com/docs/guides/function-calling?api-mode=chat)
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ repositories {

allprojects {
group = "com.openai"
version = "2.11.0" // x-release-please-version
version = "2.12.0" // x-release-please-version
}

subprojects {
Expand Down
2 changes: 2 additions & 0 deletions openai-java-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
api("com.fasterxml.jackson.core:jackson-core:2.18.2")
api("com.fasterxml.jackson.core:jackson-databind:2.18.2")
api("com.google.errorprone:error_prone_annotations:2.33.0")
api("io.swagger.core.v3:swagger-annotations:2.2.31")

implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.2")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2")
Expand All @@ -29,6 +30,7 @@ dependencies {
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")
implementation("com.github.victools:jsonschema-module-swagger-2:4.38.0")

testImplementation(kotlin("test"))
testImplementation(project(":openai-java-client-okhttp"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ private constructor(
webhookSecret = clientOptions.webhookSecret
}

fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
fun httpClient(httpClient: HttpClient) = apply {
this.httpClient = PhantomReachableClosingHttpClient(httpClient)
}

fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility
Expand Down Expand Up @@ -335,13 +337,11 @@ private constructor(

return ClientOptions(
httpClient,
PhantomReachableClosingHttpClient(
RetryingHttpClient.builder()
.httpClient(httpClient)
.clock(clock)
.maxRetries(maxRetries)
.build()
),
RetryingHttpClient.builder()
.httpClient(httpClient)
.clock(clock)
.maxRetries(maxRetries)
.build(),
checkJacksonVersionCompatibility,
jsonMapper,
streamHandlerExecutor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ internal class JsonSchemaValidator private constructor() {
private const val ENUM = "enum"
private const val ADDITIONAL_PROPS = "additionalProperties"

private const val PATTERN = "pattern"
private const val FORMAT = "format"
private const val MULTIPLE_OF = "multipleOf"
private const val MINIMUM = "minimum"
private const val EXCLUSIVE_MINIMUM = "exclusiveMinimum"
private const val MAXIMUM = "maximum"
private const val EXCLUSIVE_MAXIMUM = "exclusiveMaximum"
private const val MIN_ITEMS = "minItems"
private const val MAX_ITEMS = "maxItems"

// The names of the supported schema data types.
//
// JSON Schema does not define an "integer" type, only a "number" type, but it allows any
Expand All @@ -50,21 +60,8 @@ internal class JsonSchemaValidator private constructor() {
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 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
Expand Down Expand Up @@ -94,14 +91,40 @@ internal class JsonSchemaValidator private constructor() {
* 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)
private val ALLOWED_KEYWORDS_ARRAY_SUB_SCHEMA =
setOf(TYPE, TITLE, DESC, ITEMS, MIN_ITEMS, MAX_ITEMS)

/**
* The set of allowed keywords when defining sub-schemas when the `"type"` field is set to
* `"boolean"`, `"integer"`, `"number"`, or `"string"`.
* `"boolean"`, or any other simple type not handled separately.
*/
private val ALLOWED_KEYWORDS_SIMPLE_SUB_SCHEMA = setOf(TYPE, TITLE, DESC, ENUM, CONST)

/**
* The set of allowed keywords when defining sub-schemas when the `"type"` field is set to
* `"string"`.
*/
private val ALLOWED_KEYWORDS_STRING_SUB_SCHEMA =
setOf(TYPE, TITLE, DESC, ENUM, CONST, PATTERN, FORMAT)

/**
* The set of allowed keywords when defining sub-schemas when the `"type"` field is set to
* `"integer"` or `"number"`.
*/
private val ALLOWED_KEYWORDS_NUMBER_SUB_SCHEMA =
setOf(
TYPE,
TITLE,
DESC,
ENUM,
CONST,
MINIMUM,
EXCLUSIVE_MINIMUM,
MAXIMUM,
EXCLUSIVE_MAXIMUM,
MULTIPLE_OF,
)

/**
* 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:
Expand Down Expand Up @@ -474,7 +497,15 @@ internal class JsonSchemaValidator private constructor() {
* 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 allowedKeywords =
when (typeName) {
TYPE_NUMBER,
TYPE_INTEGER -> ALLOWED_KEYWORDS_NUMBER_SUB_SCHEMA
TYPE_STRING -> ALLOWED_KEYWORDS_STRING_SUB_SCHEMA
else -> ALLOWED_KEYWORDS_SIMPLE_SUB_SCHEMA
}

validateKeywords(schema, allowedKeywords, path, depth)

val enumField = schema.get(ENUM)

Expand Down Expand Up @@ -600,7 +631,7 @@ internal class JsonSchemaValidator private constructor() {

/**
* Validates that the names of all fields in the given schema node are present in a collection
* of allowed keywords.
* of allowed keywords. The values associated with the keywords are _not_ validated.
*
* @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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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.github.victools.jsonschema.module.swagger2.Swagger2Module
import com.openai.errors.OpenAIInvalidDataException
import com.openai.models.FunctionDefinition
import com.openai.models.ResponseFormatJsonSchema
Expand Down Expand Up @@ -201,6 +202,9 @@ internal fun extractSchema(type: Class<*>): ObjectNode {
// 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())
// Use `Swagger2Module` to support OpenAPI Swagger 2 `@Schema` annotations to set
// property constraints (e.g., a `"pattern"` constraint for a string property).
.with(Swagger2Module())

configBuilder
.forFields()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// File generated from our OpenAPI spec by Stainless.

package com.openai.core

import com.openai.core.http.HttpClient
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify

@ExtendWith(MockitoExtension::class)
internal class ClientOptionsTest {

private val httpClient = mock<HttpClient>()

@Test
fun toBuilder_whenOriginalClientOptionsGarbageCollected_doesNotCloseOriginalClient() {
var clientOptions =
ClientOptions.builder().httpClient(httpClient).apiKey("My API Key").build()
verify(httpClient, never()).close()

// Overwrite the `clientOptions` variable so that the original `ClientOptions` is GC'd.
clientOptions = clientOptions.toBuilder().build()
System.gc()
Thread.sleep(100)

verify(httpClient, never()).close()
// This exists so that `clientOptions` is still reachable.
assertThat(clientOptions).isEqualTo(clientOptions)
}
}
Loading