From c960b7430308f52317f93e0b5dc06c1ff9e8fcf1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:07:02 -0400 Subject: [PATCH] release: 3.3.0 (#606) * feat(client): support verbosity with structured outputs (#603) * verbosity: structured output support for verbosity #576 * verbosity: minor doc fixes * release: 3.3.0 --------- Co-authored-by: D Gardner Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 + README.md | 26 +++- build.gradle.kts | 2 +- .../com/openai/core/JsonSchemaValidator.kt | 2 +- .../com/openai/core/StructuredOutputs.kt | 30 ++-- .../ChatCompletionMessageFunctionToolCall.kt | 2 +- .../StructuredChatCompletionCreateParams.kt | 2 +- .../models/responses/ResponseCreateParams.kt | 17 +++ .../models/responses/ResponseTextConfig.kt | 23 +++ .../StructuredResponseCreateParams.kt | 25 ++- .../responses/StructuredResponseTextConfig.kt | 143 ++++++++++++++++++ .../openai/core/StructuredOutputsTestUtils.kt | 5 +- ...tructuredChatCompletionCreateParamsTest.kt | 2 +- .../StructuredResponseCreateParamsTest.kt | 27 +++- .../StructuredResponseTextConfigTest.kt | 130 ++++++++++++++++ ...nsesStructuredOutputsVerbosityExample.java | 77 ++++++++++ 17 files changed, 487 insertions(+), 36 deletions(-) create mode 100644 openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseTextConfig.kt create mode 100644 openai-java-core/src/test/kotlin/com/openai/models/responses/StructuredResponseTextConfigTest.kt create mode 100644 openai-java-example/src/main/java/com/openai/example/ResponsesStructuredOutputsVerbosityExample.java diff --git a/.release-please-manifest.json b/.release-please-manifest.json index a9e40ee00..ff1c7af58 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.2.1" + ".": "3.3.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dd6c075d..bf7f5d1b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 3.3.0 (2025-09-03) + +Full Changelog: [v3.2.1...v3.3.0](https://github.com/openai/openai-java/compare/v3.2.1...v3.3.0) + +### Features + +* **client:** support verbosity with structured outputs ([#603](https://github.com/openai/openai-java/issues/603)) ([2496464](https://github.com/openai/openai-java/commit/24964646ab02a3ff61efb9802facac66b204bdaf)) + ## 3.2.1 (2025-09-02) Full Changelog: [v3.2.0...v3.2.1](https://github.com/openai/openai-java/compare/v3.2.0...v3.2.1) diff --git a/README.md b/README.md index feebec7e5..ed3ee9732 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ -[![Maven Central](https://img.shields.io/maven-central/v/com.openai/openai-java)](https://central.sonatype.com/artifact/com.openai/openai-java/3.2.1) -[![javadoc](https://javadoc.io/badge2/com.openai/openai-java/3.2.1/javadoc.svg)](https://javadoc.io/doc/com.openai/openai-java/3.2.1) +[![Maven Central](https://img.shields.io/maven-central/v/com.openai/openai-java)](https://central.sonatype.com/artifact/com.openai/openai-java/3.3.0) +[![javadoc](https://javadoc.io/badge2/com.openai/openai-java/3.3.0/javadoc.svg)](https://javadoc.io/doc/com.openai/openai-java/3.3.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/3.2.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/3.3.0). @@ -24,7 +24,7 @@ The REST API documentation can be found on [platform.openai.com](https://platfor ### Gradle ```kotlin -implementation("com.openai:openai-java:3.2.1") +implementation("com.openai:openai-java:3.3.0") ``` ### Maven @@ -33,7 +33,7 @@ implementation("com.openai:openai-java:3.2.1") com.openai openai-java - 3.2.1 + 3.3.0 ``` @@ -565,6 +565,18 @@ 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). +Instead of using `ResponseCreateParams.text(Class)`, you can build a +[`StructuredResponseTextConfig`](openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseTextConfig.kt) +and set it on the `ResponseCreateParams` using the `text(StructuredResponseTextConfig)` method. +Similar to using `ResponseCreateParams`, you can start with a `ResponseTextConfig.Builder` and its +`format(Class)` method will change it to a `StructuredResponseTextConfig.Builder`. This also +allows you to set the `verbosity` configuration parameter on the text configuration before adding it +to the `ResponseCreateParams`. + +For a full example of the usage of _Structured Outputs_ with the `ResponseTextConfig` and its +`verbosity` parameter, see +[`ResponsesStructuredOutputsVerbosityExample`](openai-java-example/src/main/java/com/openai/example/ResponsesStructuredOutputsVerbosityExample.java). + ### Usage with streaming _Structured Outputs_ can also be used with [Streaming](#streaming) and the Chat Completions API. As @@ -1330,7 +1342,7 @@ If you're using Spring Boot, then you can use the SDK's [Spring Boot starter](ht #### Gradle ```kotlin -implementation("com.openai:openai-java-spring-boot-starter:3.2.1") +implementation("com.openai:openai-java-spring-boot-starter:3.3.0") ``` #### Maven @@ -1339,7 +1351,7 @@ implementation("com.openai:openai-java-spring-boot-starter:3.2.1") com.openai openai-java-spring-boot-starter - 3.2.1 + 3.3.0 ``` diff --git a/build.gradle.kts b/build.gradle.kts index 81688f786..2c2a57a42 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ repositories { allprojects { group = "com.openai" - version = "3.2.1" // x-release-please-version + version = "3.3.0" // x-release-please-version } subprojects { 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 index 87db4dacd..f0f35422f 100644 --- a/openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaValidator.kt +++ b/openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaValidator.kt @@ -312,7 +312,7 @@ internal class JsonSchemaValidator private constructor() { /** * 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 + * at the root of a JSON schema. The value of 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) { 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 index aceb5448b..b0198313d 100644 --- a/openai-java-core/src/main/kotlin/com/openai/core/StructuredOutputs.kt +++ b/openai-java-core/src/main/kotlin/com/openai/core/StructuredOutputs.kt @@ -85,6 +85,20 @@ internal fun validateSchema( return schema } +/** Builds a text configuration's JSON schema, deriving it from the structure of a class. */ +@JvmSynthetic +internal fun jsonSchemaFromClass( + type: Class<*>, + localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES, +): ResponseFormatTextJsonSchemaConfig = + ResponseFormatTextJsonSchemaConfig.builder() + .name("json-schema-from-${type.simpleName}") + .schema(JsonValue.fromJsonNode(validateSchema(extractSchema(type), 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() + /** * Builds a text configuration with its format set to a JSON schema derived from the structure of an * arbitrary Java class. @@ -94,21 +108,7 @@ internal fun textConfigFromClass( type: Class<*>, localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES, ): ResponseTextConfig = - ResponseTextConfig.builder() - .format( - ResponseFormatTextJsonSchemaConfig.builder() - .name("json-schema-from-${type.simpleName}") - .schema( - JsonValue.fromJsonNode( - validateSchema(extractSchema(type), 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() + ResponseTextConfig.builder().format(jsonSchemaFromClass(type, localValidation)).build() // "internal" instead of "private" for testing purposes. internal data class FunctionInfo( diff --git a/openai-java-core/src/main/kotlin/com/openai/models/chat/completions/ChatCompletionMessageFunctionToolCall.kt b/openai-java-core/src/main/kotlin/com/openai/models/chat/completions/ChatCompletionMessageFunctionToolCall.kt index 9b050f924..2d28d20d7 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/chat/completions/ChatCompletionMessageFunctionToolCall.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/chat/completions/ChatCompletionMessageFunctionToolCall.kt @@ -267,7 +267,7 @@ private constructor( * Gets the arguments to the function call, converting the values from the model in JSON * format to an instance of a class that holds those values. The class must previously have * been used to define the JSON schema for the function definition's parameters, so that the - * JSON corresponds to structure of the given class. + * JSON corresponds to the structure of the given class. * * @throws OpenAIInvalidDataException If the JSON data is missing, `null`, or cannot be * parsed to an instance of the [functionParametersType] class. This might occur if the diff --git a/openai-java-core/src/main/kotlin/com/openai/models/chat/completions/StructuredChatCompletionCreateParams.kt b/openai-java-core/src/main/kotlin/com/openai/models/chat/completions/StructuredChatCompletionCreateParams.kt index 5809e2e87..fd9c0eefe 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/chat/completions/StructuredChatCompletionCreateParams.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/chat/completions/StructuredChatCompletionCreateParams.kt @@ -15,7 +15,7 @@ import java.util.Optional /** * A wrapper for [ChatCompletionCreateParams] that provides a type-safe [Builder] that can record * the [responseType] used to derive a JSON schema from an arbitrary class when using the - * _Structured Outputs_ feature. When a JSON response is received, it is deserialized to am instance + * _Structured Outputs_ feature. When a JSON response is received, it is deserialized to an instance * of that type. See the SDK documentation for more details on _Structured Outputs_. * * @param T The type of the class that will be used to derive the JSON schema in the request and to diff --git a/openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseCreateParams.kt b/openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseCreateParams.kt index 212b9af9d..d842e305c 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseCreateParams.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseCreateParams.kt @@ -1098,6 +1098,8 @@ private constructor( * [StructuredResponseCreateParams.Builder] that will build a * [StructuredResponseCreateParams] instance when `build()` is called. * + * Use this method or the `text(StructuredResponseTextConfig)` method, but not both. + * * @param responseType A class from which a JSON schema will be derived to define the text * configuration's format. * @param localValidation [JsonSchemaLocalValidation.YES] (the default) to validate the JSON @@ -1114,6 +1116,21 @@ private constructor( localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES, ) = StructuredResponseCreateParams.builder().wrap(responseType, this, localValidation) + /** + * Sets the text configuration to a [StructuredResponseTextConfig] where the format was set + * to a JSON schema derived from the structure of a class. This changes the builder to a + * type-safe [StructuredResponseCreateParams.Builder] that will build a + * [StructuredResponseCreateParams] instance when `build()` is called. + * + * Use this method or the `text(Class)` method, but not both. + * + * @param text A text configuration in which the JSON schema defining the format was derived + * from the structure of a class. The `verbosity` parameter can also be set on the text + * configuration, if required. + */ + fun text(text: StructuredResponseTextConfig) = + StructuredResponseCreateParams.builder().wrap(text, this) + /** * How the model should select which tool (or tools) to use when generating a response. See * the `tools` parameter to see how to specify which tools the model can call. diff --git a/openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseTextConfig.kt b/openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseTextConfig.kt index 03bbec9be..e68b81015 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseTextConfig.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseTextConfig.kt @@ -10,6 +10,7 @@ import com.openai.core.Enum import com.openai.core.ExcludeMissing import com.openai.core.JsonField import com.openai.core.JsonMissing +import com.openai.core.JsonSchemaLocalValidation import com.openai.core.JsonValue import com.openai.errors.OpenAIInvalidDataException import com.openai.models.ResponseFormatJsonObject @@ -157,6 +158,28 @@ private constructor( fun format(jsonObject: ResponseFormatJsonObject) = format(ResponseFormatTextConfig.ofJsonObject(jsonObject)) + /** + * Sets the text configuration's format to a JSON schema derived from the structure of the + * given class. This changes the builder to a type-safe + * [StructuredResponseTextConfig.Builder] that will build a [StructuredResponseTextConfig] + * instance when `build()` is called. + * + * @param responseType A class from which a JSON schema will be derived to define the text + * configuration's format. + * @param localValidation [JsonSchemaLocalValidation.YES] (the default) to validate the JSON + * schema locally when it is generated by this method to confirm that it adheres to the + * requirements and restrictions on JSON schemas imposed by the OpenAI specification; or + * [JsonSchemaLocalValidation.NO] to skip local validation and rely only on remote + * validation. See the SDK documentation for more details. + * @throws IllegalArgumentException If local validation is enabled, but it fails because a + * valid JSON schema cannot be derived from the given class. + */ + @JvmOverloads + fun format( + responseType: Class, + localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES, + ) = StructuredResponseTextConfig.builder().wrap(responseType, this, localValidation) + /** * Constrains the verbosity of the model's response. Lower values will result in more * concise responses, while higher values will result in more verbose responses. Currently diff --git a/openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseCreateParams.kt b/openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseCreateParams.kt index 80b5a1f25..0568621c8 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseCreateParams.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseCreateParams.kt @@ -16,7 +16,7 @@ import java.util.Optional /** * A wrapper for [ResponseCreateParams] that provides a type-safe [Builder] that can record the * [responseType] used to derive a JSON schema from an arbitrary class when using the _Structured - * Outputs_ feature. When a JSON response is received, it is deserialized to am instance of that + * Outputs_ feature. When a JSON response is received, it is deserialized to an instance of that * type. See the SDK documentation for more details on _Structured Outputs_. * * @param T The type of the class that will be used to derive the JSON schema in the request and to @@ -51,6 +51,16 @@ class StructuredResponseCreateParams( text(responseType, localValidation) } + @JvmSynthetic + internal fun wrap( + textConfig: StructuredResponseTextConfig, + paramsBuilder: ResponseCreateParams.Builder, + ) = apply { + this.responseType = textConfig.responseType + this.paramsBuilder = paramsBuilder + text(textConfig) + } + /** Injects a given `ResponseCreateParams.Builder`. For use only when testing. */ @JvmSynthetic internal fun inject(paramsBuilder: ResponseCreateParams.Builder) = apply { @@ -342,6 +352,17 @@ class StructuredResponseCreateParams( paramsBuilder.text(textConfigFromClass(responseType, localValidation)) } + /** + * Sets the text configuration to a [StructuredResponseTextConfig] where the format was set + * to a JSON schema derived from the structure of a class. + * + * @see ResponseCreateParams.Builder.text + */ + fun text(text: StructuredResponseTextConfig) = apply { + this.responseType = text.responseType + paramsBuilder.text(text.rawConfig) + } + /** @see ResponseCreateParams.Builder.toolChoice */ fun toolChoice(toolChoice: ResponseCreateParams.ToolChoice) = apply { paramsBuilder.toolChoice(toolChoice) @@ -665,7 +686,7 @@ class StructuredResponseCreateParams( } /** - * Returns an immutable instance of [ResponseCreateParams]. + * Returns an immutable instance of [StructuredResponseCreateParams]. * * Further updates to this [Builder] will not mutate the returned instance. * diff --git a/openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseTextConfig.kt b/openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseTextConfig.kt new file mode 100644 index 000000000..050def9cb --- /dev/null +++ b/openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseTextConfig.kt @@ -0,0 +1,143 @@ +// File generated from our OpenAPI spec by Stainless. + +package com.openai.models.responses + +import com.openai.core.JsonField +import com.openai.core.JsonSchemaLocalValidation +import com.openai.core.JsonValue +import com.openai.core.checkRequired +import com.openai.core.jsonSchemaFromClass +import java.util.Objects +import java.util.Optional + +/** + * A wrapper for [ResponseTextConfig] that provides a type-safe [Builder] that can record the + * [responseType] used to derive a JSON schema from an arbitrary class when using the _Structured + * Outputs_ feature. When a JSON response is received, it is deserialized to an instance of that + * type. See the SDK documentation for more details on _Structured Outputs_. + * + * @param T The type of the class that will be used to derive the JSON schema in the request and to + * which the JSON response will be deserialized. + */ +class StructuredResponseTextConfig +private constructor( + @get:JvmName("responseType") val responseType: Class, + /** + * The raw, underlying response text configuration wrapped by this structured instance of the + * configuration. + */ + @get:JvmName("rawConfig") val rawConfig: ResponseTextConfig, +) { + companion object { + /** + * Returns a mutable builder for constructing an instance of [StructuredResponseTextConfig]. + */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [StructuredResponseTextConfig]. */ + class Builder internal constructor() { + private var responseType: Class? = null + private var configBuilder = ResponseTextConfig.builder() + + @JvmSynthetic + internal fun wrap( + responseType: Class, + configBuilder: ResponseTextConfig.Builder, + localValidation: JsonSchemaLocalValidation, + ) = apply { + this.responseType = responseType + this.configBuilder = configBuilder + format(responseType, localValidation) + } + + /** Injects a given `ResponseTextConfig.Builder`. For use only when testing. */ + @JvmSynthetic + internal fun inject(configBuilder: ResponseTextConfig.Builder) = apply { + this.configBuilder = configBuilder + } + + /** + * Sets the text configuration's format to a JSON schema derived from the structure of the + * given class. + * + * @see ResponseTextConfig.Builder.format + */ + @JvmOverloads + fun format( + responseType: Class, + localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES, + ) = apply { + this.responseType = responseType + configBuilder.format(jsonSchemaFromClass(responseType, localValidation)) + } + + /** @see ResponseTextConfig.Builder.verbosity */ + fun verbosity(verbosity: ResponseTextConfig.Verbosity?) = apply { + configBuilder.verbosity(verbosity) + } + + /** @see ResponseTextConfig.Builder.verbosity */ + fun verbosity(verbosity: Optional) = apply { + configBuilder.verbosity(verbosity) + } + + /** @see ResponseTextConfig.Builder.verbosity */ + fun verbosity(verbosity: JsonField) = apply { + configBuilder.verbosity(verbosity) + } + + /** @see ResponseTextConfig.Builder.additionalProperties */ + fun additionalProperties(additionalProperties: Map) = apply { + configBuilder.additionalProperties(additionalProperties) + } + + /** @see ResponseTextConfig.Builder.putAdditionalProperty */ + fun putAdditionalProperty(key: String, value: JsonValue) = apply { + configBuilder.putAdditionalProperty(key, value) + } + + /** @see ResponseTextConfig.Builder.putAllAdditionalProperties */ + fun putAllAdditionalProperties(additionalProperties: Map) = apply { + configBuilder.putAllAdditionalProperties(additionalProperties) + } + + /** @see ResponseTextConfig.Builder.removeAdditionalProperty */ + fun removeAdditionalProperty(key: String) = apply { + configBuilder.removeAdditionalProperty(key) + } + + /** @see ResponseTextConfig.Builder.removeAllAdditionalProperties */ + fun removeAllAdditionalProperties(keys: Set) = apply { + configBuilder.removeAllAdditionalProperties(keys) + } + + /** + * Returns an immutable instance of [StructuredResponseTextConfig]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): StructuredResponseTextConfig = + StructuredResponseTextConfig( + checkRequired("responseType", responseType), + configBuilder.build(), + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is StructuredResponseTextConfig<*> && + responseType == other.responseType && + rawConfig == other.rawConfig + } + + private val hashCode: Int by lazy { Objects.hash(responseType, rawConfig) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "${javaClass.simpleName}{responseType=$responseType, rawConfig=$rawConfig}" +} diff --git a/openai-java-core/src/test/kotlin/com/openai/core/StructuredOutputsTestUtils.kt b/openai-java-core/src/test/kotlin/com/openai/core/StructuredOutputsTestUtils.kt index 267e17d46..54945fba6 100644 --- a/openai-java-core/src/test/kotlin/com/openai/core/StructuredOutputsTestUtils.kt +++ b/openai-java-core/src/test/kotlin/com/openai/core/StructuredOutputsTestUtils.kt @@ -321,14 +321,15 @@ internal fun checkAllDelegatorReadFunctionsAreTested( * @param delegationTestCases The tests cases that identify the names of delegating functions for * which parameterized unit tests have been defined. * @param exceptionalTestedFns The names of delegating functions that are tested separately, not as - * parameterized unit tests. This is usually because they require special handling in the test. + * parameterized unit tests. This is usually because they require special handling in the test. If + * functions are overloaded, repeat the name for of the function for each overload. * @param nonDelegatingFns The names of functions that do not perform any delegation and for which * delegation tests are not required. */ internal fun checkAllDelegatorWriteFunctionsAreTested( delegatorClass: KClass<*>, delegationTestCases: List, - exceptionalTestedFns: Set, + exceptionalTestedFns: List, nonDelegatingFns: Set, ) { // There are exceptional test cases for some functions. Most other functions are part of the diff --git a/openai-java-core/src/test/kotlin/com/openai/models/chat/completions/StructuredChatCompletionCreateParamsTest.kt b/openai-java-core/src/test/kotlin/com/openai/models/chat/completions/StructuredChatCompletionCreateParamsTest.kt index 1283732b3..14c54bfc2 100644 --- a/openai-java-core/src/test/kotlin/com/openai/models/chat/completions/StructuredChatCompletionCreateParamsTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/models/chat/completions/StructuredChatCompletionCreateParamsTest.kt @@ -337,7 +337,7 @@ internal class StructuredChatCompletionCreateParamsTest { checkAllDelegatorWriteFunctionsAreTested( builderDelegator::class, builderDelegationTestCases(), - exceptionalTestedFns = setOf("responseFormat"), + exceptionalTestedFns = listOf("responseFormat"), nonDelegatingFns = setOf("build", "wrap", "inject"), ) } diff --git a/openai-java-core/src/test/kotlin/com/openai/models/responses/StructuredResponseCreateParamsTest.kt b/openai-java-core/src/test/kotlin/com/openai/models/responses/StructuredResponseCreateParamsTest.kt index c711cff69..fa9b3ba43 100644 --- a/openai-java-core/src/test/kotlin/com/openai/models/responses/StructuredResponseCreateParamsTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/models/responses/StructuredResponseCreateParamsTest.kt @@ -284,8 +284,8 @@ internal class StructuredResponseCreateParamsTest { // For Structured Outputs, setting `body` would overwrite the previously set `text` // property, which would break the Structured Outputs behavior. "body", - // For Structured Outputs, a new type-safe generic`text` function replaces all existing - // text functions, as they are mutually incompatible. This function has its own + // For Structured Outputs, new type-safe generic `text` functions replace all existing + // `text` functions, as they are mutually incompatible. These functions have their own // dedicated unit tests. "text", ) @@ -303,7 +303,7 @@ internal class StructuredResponseCreateParamsTest { checkAllDelegatorWriteFunctionsAreTested( builderDelegator::class, builderDelegationTestCases(), - exceptionalTestedFns = setOf("text"), + exceptionalTestedFns = listOf("text", "text"), // Two overloads. Two custom tests below. nonDelegatingFns = setOf("build", "wrap", "inject"), ) } @@ -315,7 +315,7 @@ internal class StructuredResponseCreateParamsTest { } @Test - fun `delegation of text`() { + fun `delegation of text with class`() { // Special unit test case as the delegator method signature does not match that of the // delegate method. val delegatorTestCase = DelegationWriteTestCase("text", X::class.java) @@ -332,4 +332,23 @@ internal class StructuredResponseCreateParamsTest { } verifyNoMoreInteractions(mockBuilderDelegate) } + + @Test + fun `delegation of text with text config`() { + // Special unit test case as the delegator method signature does not match that of the + // delegate method. + val textConfig = StructuredResponseTextConfig.builder().format(X::class.java).build() + val delegatorTestCase = DelegationWriteTestCase("text", textConfig) + val delegatorMethod = findDelegationMethod(builderDelegator, delegatorTestCase) + val mockDelegateTestCase = DelegationWriteTestCase("text", textConfig.rawConfig) + val mockDelegateMethod = findDelegationMethod(mockBuilderDelegate, mockDelegateTestCase) + + delegatorMethod.invoke(builderDelegator, delegatorTestCase.inputValues[0]) + + // Verify that the corresponding method on the mock delegate was called exactly once. + verify(mockBuilderDelegate, times(1)).apply { + mockDelegateMethod.invoke(mockBuilderDelegate, mockDelegateTestCase.inputValues[0]) + } + verifyNoMoreInteractions(mockBuilderDelegate) + } } diff --git a/openai-java-core/src/test/kotlin/com/openai/models/responses/StructuredResponseTextConfigTest.kt b/openai-java-core/src/test/kotlin/com/openai/models/responses/StructuredResponseTextConfigTest.kt new file mode 100644 index 000000000..f4f348fbb --- /dev/null +++ b/openai-java-core/src/test/kotlin/com/openai/models/responses/StructuredResponseTextConfigTest.kt @@ -0,0 +1,130 @@ +package com.openai.models.responses + +import com.openai.core.DelegationWriteTestCase +import com.openai.core.JSON_FIELD +import com.openai.core.JSON_VALUE +import com.openai.core.MAP +import com.openai.core.OPTIONAL +import com.openai.core.SET +import com.openai.core.STRING +import com.openai.core.X +import com.openai.core.checkAllDelegation +import com.openai.core.checkAllDelegatorWriteFunctionsAreTested +import com.openai.core.checkOneDelegationWrite +import com.openai.core.findDelegationMethod +import com.openai.core.jsonSchemaFromClass +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.Mockito.mock +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +/** + * Unit tests for the [StructuredResponseTextConfig] class (delegator) and its delegation of most + * functions to a wrapped [ResponseTextConfig] (delegate). The tests include confirmation of the + * following: + * - All functions in the delegator correspond to a function in the delegate and _vice versa_. + * - All functions in the delegator call their corresponding function in the delegate and only that + * function. + * - A unit test exists for all functions. + * + * There are some exceptions to the above that are handled differently. + */ +internal class StructuredResponseTextConfigTest { + companion object { + private val VERBOSITY = ResponseTextConfig.Verbosity.HIGH + + // The list order follows the declaration order in `ResponseTextConfig.Builder` for easier + // maintenance. + @JvmStatic + private fun builderDelegationTestCases() = + listOf( + // The `format` function is a special case and is handled separately. + DelegationWriteTestCase("verbosity", VERBOSITY), + DelegationWriteTestCase("verbosity", OPTIONAL), + DelegationWriteTestCase("verbosity", JSON_FIELD), + DelegationWriteTestCase("additionalProperties", MAP), + DelegationWriteTestCase("putAdditionalProperty", STRING, JSON_VALUE), + DelegationWriteTestCase("putAllAdditionalProperties", MAP), + DelegationWriteTestCase("removeAdditionalProperty", STRING), + DelegationWriteTestCase("removeAllAdditionalProperties", SET), + ) + } + + // New instances of the `mockBuilderDelegate` and `builderDelegator` are required for each test + // case (each test case runs in its own instance of the test class). + private val mockBuilderDelegate: ResponseTextConfig.Builder = + mock(ResponseTextConfig.Builder::class.java) + private val builderDelegator = + StructuredResponseTextConfig.builder().inject(mockBuilderDelegate) + + @Test + fun allBuilderDelegateFunctionsExistInDelegator() { + checkAllDelegation( + mockBuilderDelegate::class, + builderDelegator::class, + // ************************************************************************************ + // NOTE: THIS TEST EXISTS TO ENSURE THAT WHEN NEW FUNCTIONS ARE ADDED MANUALLY OR VIA + // CODE GEN TO `ResponseTextConfig.Builder`, THAT THOSE FUNCTIONS ARE _ALSO_ ADDED + // _MANUALLY_ TO `StructuredResponseTextConfig.Builder`. FAILURE TO ADD THOSE FUNCTIONS + // RESULTS IN _MISSING_ FUNCTIONALITY WHEN USING STRUCTURED OUTPUTS. EXCEPTIONS ADDED TO + // THIS LIST ARE PRESENT BY DESIGN, NOT BECAUSE THE FUNCTIONS ARE SIMPLY NOT YET + // IMPLEMENTED IN THE DELEGATOR CLASS. + // + // DO NOT ADD EXCEPTIONS TO THIS LIST SIMPLY BECAUSE TESTS ARE FAILING. THE TESTS ARE + // SUPPOSED TO FAIL. ADD THE NEW FUNCTIONS TO `StructuredResponseTextConfig.Builder` + // AND ADD A PARAMETERIZED TEST TO `builderDelegationTestCases` (above) TO ENSURE + // CORRECT DELEGATION BEHAVIOR. + // ************************************************************************************ + + // For Structured Outputs, a new type-safe generic `format` function replaces all + // existing `format` functions, as they are mutually incompatible. This function has its + // own dedicated unit tests. + "format", + ) + } + + @Test + fun allBuilderDelegatorFunctionsExistInDelegate() { + // The delegator implements a different `text` function from those overloads in the delegate + // class. + checkAllDelegation(builderDelegator::class, mockBuilderDelegate::class, "text") + } + + @Test + fun allBuilderDelegatorFunctionsAreTested() { + checkAllDelegatorWriteFunctionsAreTested( + builderDelegator::class, + builderDelegationTestCases(), + exceptionalTestedFns = listOf("format"), + nonDelegatingFns = setOf("build", "wrap", "inject"), + ) + } + + @ParameterizedTest + @MethodSource("builderDelegationTestCases") + fun `delegation of Builder write functions`(testCase: DelegationWriteTestCase) { + checkOneDelegationWrite(builderDelegator, mockBuilderDelegate, testCase) + } + + @Test + fun `delegation of format from class`() { + // Special unit test case as the delegator method signature does not match that of the + // delegate method. + val delegatorTestCase = DelegationWriteTestCase("format", X::class.java) + val delegatorMethod = findDelegationMethod(builderDelegator, delegatorTestCase) + val mockDelegateTestCase = + DelegationWriteTestCase("format", jsonSchemaFromClass(X::class.java)) + val mockDelegateMethod = findDelegationMethod(mockBuilderDelegate, mockDelegateTestCase) + + delegatorMethod.invoke(builderDelegator, delegatorTestCase.inputValues[0]) + + // Verify that the corresponding method on the mock delegate was called exactly once. + verify(mockBuilderDelegate, times(1)).apply { + mockDelegateMethod.invoke(mockBuilderDelegate, mockDelegateTestCase.inputValues[0]) + } + verifyNoMoreInteractions(mockBuilderDelegate) + } +} diff --git a/openai-java-example/src/main/java/com/openai/example/ResponsesStructuredOutputsVerbosityExample.java b/openai-java-example/src/main/java/com/openai/example/ResponsesStructuredOutputsVerbosityExample.java new file mode 100644 index 000000000..5bdb33282 --- /dev/null +++ b/openai-java-example/src/main/java/com/openai/example/ResponsesStructuredOutputsVerbosityExample.java @@ -0,0 +1,77 @@ +package com.openai.example; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.openai.client.OpenAIClient; +import com.openai.client.okhttp.OpenAIOkHttpClient; +import com.openai.models.ChatModel; +import com.openai.models.responses.ResponseCreateParams; +import com.openai.models.responses.ResponseTextConfig; +import com.openai.models.responses.StructuredResponseCreateParams; +import java.util.List; + +public final class ResponsesStructuredOutputsVerbosityExample { + + public static 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; + + @Override + public String toString() { + return name + " (" + birthYear + '-' + deathYear + ')'; + } + } + + public static class Book { + public String title; + + public Person author; + + @JsonPropertyDescription("The year in which the book was first published.") + public int publicationYear; + + public String genre; + + @JsonIgnore + public String isbn; + + @Override + public String toString() { + return '"' + title + "\" (" + publicationYear + ") [" + genre + "] by " + author; + } + } + + public static class BookList { + public List books; + } + + private ResponsesStructuredOutputsVerbosityExample() {} + + public static void main(String[] args) { + // Configures using one of: + // - The `OPENAI_API_KEY` environment variable + // - The `OPENAI_BASE_URL` and `AZURE_OPENAI_KEY` environment variables + OpenAIClient client = OpenAIOkHttpClient.fromEnv(); + + StructuredResponseCreateParams createParams = ResponseCreateParams.builder() + .input("List some famous late twentieth century novels.") + .text(ResponseTextConfig.builder() + .verbosity(ResponseTextConfig.Verbosity.HIGH) + .format(BookList.class) + .build()) + .model(ChatModel.GPT_5) + .build(); + + client.responses().create(createParams).output().stream() + .flatMap(item -> item.message().stream()) + .flatMap(message -> message.content().stream()) + .flatMap(content -> content.outputText().stream()) + .flatMap(bookList -> bookList.books.stream()) + .forEach(book -> System.out.println(" - " + book)); + } +}