Skip to content

Commit 9d47c57

Browse files
committed
verbosity: structured output support for verbosity #576
1 parent 2d81455 commit 9d47c57

14 files changed

+471
-27
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,19 @@ the latter when `ResponseCreateParams.Builder.text(Class<T>)` is called.
565565
For a full example of the usage of _Structured Outputs_ with the Responses API, see
566566
[`ResponsesStructuredOutputsExample`](openai-java-example/src/main/java/com/openai/example/ResponsesStructuredOutputsExample.java).
567567

568+
Instead of using `ResponseCreateParams.text(Class<T>)`, you can build a
569+
[`StructuredResponseTextConfig`](openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseTextConfig.kt)
570+
and set it on the `ResponseCreateParams` using the `text(StructuredResponseCreateParams)` method.
571+
Similar to using `ResponseCreateParams`, you can start with a `ResponseTextConfig.Builder` and its
572+
`format(Class<T>)` method will change it to a `StructuredResponseTextConfig.Builder`. This also
573+
allows you to set the `verbosity` configuration parameter on the text configuration before adding it
574+
to the `ResponseCreateParams`.
575+
576+
For a full example of the usage of _Structured Outputs_ with the `ResponseTextConfig` and its
577+
`verbosity` parameter, see
578+
[`ResponsesStructuredOutputsVerbosityExample`](openai-java-example/src/main/java/com/openai/example/ResponsesStructuredOutputsVerbosityExample.java).
579+
580+
568581
### Usage with streaming
569582

570583
_Structured Outputs_ can also be used with [Streaming](#streaming) and the Chat Completions API. As

openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaValidator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ internal class JsonSchemaValidator private constructor() {
312312

313313
/**
314314
* Validates a schema if it has an `"anyOf"` field. OpenAI does not support the use of `"anyOf"`
315-
* at the root of a JSON schema. The value is the field is expected to be an array of valid
315+
* at the root of a JSON schema. The value of the field is expected to be an array of valid
316316
* sub-schemas. If the schema has no `"anyOf"` field, no action is taken.
317317
*/
318318
private fun validateAnyOfSchema(schema: JsonNode, path: String, depth: Int) {

openai-java-core/src/main/kotlin/com/openai/core/StructuredOutputs.kt

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,20 @@ internal fun validateSchema(
8585
return schema
8686
}
8787

88+
/** Builds a text configuration's JSON schema, deriving it from the structure of a class. */
89+
@JvmSynthetic
90+
internal fun jsonSchemaFromClass(
91+
type: Class<*>,
92+
localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES,
93+
): ResponseFormatTextJsonSchemaConfig =
94+
ResponseFormatTextJsonSchemaConfig.builder()
95+
.name("json-schema-from-${type.simpleName}")
96+
.schema(JsonValue.fromJsonNode(validateSchema(extractSchema(type), type, localValidation)))
97+
// Ensure the model's output strictly adheres to this JSON schema. This is the essential
98+
// "ON switch" for Structured Outputs.
99+
.strict(true)
100+
.build()
101+
88102
/**
89103
* Builds a text configuration with its format set to a JSON schema derived from the structure of an
90104
* arbitrary Java class.
@@ -94,21 +108,7 @@ internal fun textConfigFromClass(
94108
type: Class<*>,
95109
localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES,
96110
): ResponseTextConfig =
97-
ResponseTextConfig.builder()
98-
.format(
99-
ResponseFormatTextJsonSchemaConfig.builder()
100-
.name("json-schema-from-${type.simpleName}")
101-
.schema(
102-
JsonValue.fromJsonNode(
103-
validateSchema(extractSchema(type), type, localValidation)
104-
)
105-
)
106-
// Ensure the model's output strictly adheres to this JSON schema. This is the
107-
// essential "ON switch" for Structured Outputs.
108-
.strict(true)
109-
.build()
110-
)
111-
.build()
111+
ResponseTextConfig.builder().format(jsonSchemaFromClass(type, localValidation)).build()
112112

113113
// "internal" instead of "private" for testing purposes.
114114
internal data class FunctionInfo(

openai-java-core/src/main/kotlin/com/openai/models/chat/completions/ChatCompletionMessageFunctionToolCall.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ private constructor(
267267
* Gets the arguments to the function call, converting the values from the model in JSON
268268
* format to an instance of a class that holds those values. The class must previously have
269269
* been used to define the JSON schema for the function definition's parameters, so that the
270-
* JSON corresponds to structure of the given class.
270+
* JSON corresponds to the structure of the given class.
271271
*
272272
* @throws OpenAIInvalidDataException If the JSON data is missing, `null`, or cannot be
273273
* parsed to an instance of the [functionParametersType] class. This might occur if the

openai-java-core/src/main/kotlin/com/openai/models/chat/completions/StructuredChatCompletionCreateParams.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import java.util.Optional
1515
/**
1616
* A wrapper for [ChatCompletionCreateParams] that provides a type-safe [Builder] that can record
1717
* the [responseType] used to derive a JSON schema from an arbitrary class when using the
18-
* _Structured Outputs_ feature. When a JSON response is received, it is deserialized to am instance
18+
* _Structured Outputs_ feature. When a JSON response is received, it is deserialized to an instance
1919
* of that type. See the SDK documentation for more details on _Structured Outputs_.
2020
*
2121
* @param T The type of the class that will be used to derive the JSON schema in the request and to

openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseCreateParams.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,8 @@ private constructor(
10981098
* [StructuredResponseCreateParams.Builder] that will build a
10991099
* [StructuredResponseCreateParams] instance when `build()` is called.
11001100
*
1101+
* Use this method or the `text(StructuredResponseTextConfig<T>)` method, but not both.
1102+
*
11011103
* @param responseType A class from which a JSON schema will be derived to define the text
11021104
* configuration's format.
11031105
* @param localValidation [JsonSchemaLocalValidation.YES] (the default) to validate the JSON
@@ -1114,6 +1116,21 @@ private constructor(
11141116
localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES,
11151117
) = StructuredResponseCreateParams.builder<T>().wrap(responseType, this, localValidation)
11161118

1119+
/**
1120+
* Sets the text configuration to a [StructuredResponseTextConfig] where the format was set
1121+
* to a JSON schema derived from the structure of a class. This changes the builder to a
1122+
* type-safe [StructuredResponseCreateParams.Builder] that will build a
1123+
* [StructuredResponseCreateParams] instance when `build()` is called.
1124+
*
1125+
* Use this method or the `text(Class<T>)` method, but not both.
1126+
*
1127+
* @param text A text configuration in which the JSON schema defining the format was derived
1128+
* from the structure of a class. The `verbosity` parameter can also be set on the text
1129+
* configuration, if required.
1130+
*/
1131+
fun <T : Any> text(text: StructuredResponseTextConfig<T>) =
1132+
StructuredResponseCreateParams.builder<T>().wrap(text, this)
1133+
11171134
/**
11181135
* How the model should select which tool (or tools) to use when generating a response. See
11191136
* the `tools` parameter to see how to specify which tools the model can call.

openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseTextConfig.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.openai.core.Enum
1010
import com.openai.core.ExcludeMissing
1111
import com.openai.core.JsonField
1212
import com.openai.core.JsonMissing
13+
import com.openai.core.JsonSchemaLocalValidation
1314
import com.openai.core.JsonValue
1415
import com.openai.errors.OpenAIInvalidDataException
1516
import com.openai.models.ResponseFormatJsonObject
@@ -157,6 +158,28 @@ private constructor(
157158
fun format(jsonObject: ResponseFormatJsonObject) =
158159
format(ResponseFormatTextConfig.ofJsonObject(jsonObject))
159160

161+
/**
162+
* Sets the text configuration's format to a JSON schema derived from the structure of the
163+
* given class. This changes the builder to a type-safe
164+
* [StructuredResponseTextConfig.Builder] that will build a [StructuredResponseTextConfig]
165+
* instance when `build()` is called.
166+
*
167+
* @param responseType A class from which a JSON schema will be derived to define the text
168+
* configuration's format.
169+
* @param localValidation [JsonSchemaLocalValidation.YES] (the default) to validate the JSON
170+
* schema locally when it is generated by this method to confirm that it adheres to the
171+
* requirements and restrictions on JSON schemas imposed by the OpenAI specification; or
172+
* [JsonSchemaLocalValidation.NO] to skip local validation and rely only on remote
173+
* validation. See the SDK documentation for more details.
174+
* @throws IllegalArgumentException If local validation is enabled, but it fails because a
175+
* valid JSON schema cannot be derived from the given class.
176+
*/
177+
@JvmOverloads
178+
fun <T : Any> format(
179+
responseType: Class<T>,
180+
localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES,
181+
) = StructuredResponseTextConfig.builder<T>().wrap(responseType, this, localValidation)
182+
160183
/**
161184
* Constrains the verbosity of the model's response. Lower values will result in more
162185
* concise responses, while higher values will result in more verbose responses. Currently

openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseCreateParams.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import java.util.Optional
1616
/**
1717
* A wrapper for [ResponseCreateParams] that provides a type-safe [Builder] that can record the
1818
* [responseType] used to derive a JSON schema from an arbitrary class when using the _Structured
19-
* Outputs_ feature. When a JSON response is received, it is deserialized to am instance of that
19+
* Outputs_ feature. When a JSON response is received, it is deserialized to an instance of that
2020
* type. See the SDK documentation for more details on _Structured Outputs_.
2121
*
2222
* @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<T : Any>(
5151
text(responseType, localValidation)
5252
}
5353

54+
@JvmSynthetic
55+
internal fun wrap(
56+
textConfig: StructuredResponseTextConfig<T>,
57+
paramsBuilder: ResponseCreateParams.Builder,
58+
) = apply {
59+
this.responseType = textConfig.responseType
60+
this.paramsBuilder = paramsBuilder
61+
text(textConfig)
62+
}
63+
5464
/** Injects a given `ResponseCreateParams.Builder`. For use only when testing. */
5565
@JvmSynthetic
5666
internal fun inject(paramsBuilder: ResponseCreateParams.Builder) = apply {
@@ -342,6 +352,17 @@ class StructuredResponseCreateParams<T : Any>(
342352
paramsBuilder.text(textConfigFromClass(responseType, localValidation))
343353
}
344354

355+
/**
356+
* Sets the text configuration to a [StructuredResponseTextConfig] where the format was set
357+
* to a JSON schema derived from the structure of a class.
358+
*
359+
* @see ResponseCreateParams.Builder.text
360+
*/
361+
fun text(text: StructuredResponseTextConfig<T>) = apply {
362+
this.responseType = text.responseType
363+
paramsBuilder.text(text.rawConfig)
364+
}
365+
345366
/** @see ResponseCreateParams.Builder.toolChoice */
346367
fun toolChoice(toolChoice: ResponseCreateParams.ToolChoice) = apply {
347368
paramsBuilder.toolChoice(toolChoice)
@@ -665,7 +686,7 @@ class StructuredResponseCreateParams<T : Any>(
665686
}
666687

667688
/**
668-
* Returns an immutable instance of [ResponseCreateParams].
689+
* Returns an immutable instance of [StructuredResponseCreateParams].
669690
*
670691
* Further updates to this [Builder] will not mutate the returned instance.
671692
*
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// File generated from our OpenAPI spec by Stainless.
2+
3+
package com.openai.models.responses
4+
5+
import com.openai.core.JsonField
6+
import com.openai.core.JsonSchemaLocalValidation
7+
import com.openai.core.JsonValue
8+
import com.openai.core.checkRequired
9+
import com.openai.core.jsonSchemaFromClass
10+
import java.util.Objects
11+
import java.util.Optional
12+
13+
/**
14+
* A wrapper for [ResponseTextConfig] that provides a type-safe [Builder] that can record the
15+
* [responseType] used to derive a JSON schema from an arbitrary class when using the _Structured
16+
* Outputs_ feature. When a JSON response is received, it is deserialized to an instance of that
17+
* type. See the SDK documentation for more details on _Structured Outputs_.
18+
*
19+
* @param T The type of the class that will be used to derive the JSON schema in the request and to
20+
* which the JSON response will be deserialized.
21+
*/
22+
class StructuredResponseTextConfig<T : Any>
23+
private constructor(
24+
@get:JvmName("responseType") val responseType: Class<T>,
25+
/**
26+
* The raw, underlying response text configuration wrapped by this structured instance of the
27+
* configuration.
28+
*/
29+
@get:JvmName("rawConfig") val rawConfig: ResponseTextConfig,
30+
) {
31+
companion object {
32+
/**
33+
* Returns a mutable builder for constructing an instance of [StructuredResponseTextConfig].
34+
*/
35+
@JvmStatic fun <T : Any> builder() = Builder<T>()
36+
}
37+
38+
/** A builder for [StructuredResponseTextConfig]. */
39+
class Builder<T : Any> internal constructor() {
40+
private var responseType: Class<T>? = null
41+
private var configBuilder = ResponseTextConfig.builder()
42+
43+
@JvmSynthetic
44+
internal fun wrap(
45+
responseType: Class<T>,
46+
configBuilder: ResponseTextConfig.Builder,
47+
localValidation: JsonSchemaLocalValidation,
48+
) = apply {
49+
this.responseType = responseType
50+
this.configBuilder = configBuilder
51+
format(responseType, localValidation)
52+
}
53+
54+
/** Injects a given `ResponseTextConfig.Builder`. For use only when testing. */
55+
@JvmSynthetic
56+
internal fun inject(configBuilder: ResponseTextConfig.Builder) = apply {
57+
this.configBuilder = configBuilder
58+
}
59+
60+
/**
61+
* Sets the text configuration's format to a JSON schema derived from the structure of the
62+
* given class.
63+
*
64+
* @see ResponseTextConfig.Builder.format
65+
*/
66+
@JvmOverloads
67+
fun format(
68+
responseType: Class<T>,
69+
localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES,
70+
) = apply {
71+
this.responseType = responseType
72+
configBuilder.format(jsonSchemaFromClass(responseType, localValidation))
73+
}
74+
75+
/** @see ResponseTextConfig.Builder.verbosity */
76+
fun verbosity(verbosity: ResponseTextConfig.Verbosity?) = apply {
77+
configBuilder.verbosity(verbosity)
78+
}
79+
80+
/** @see ResponseTextConfig.Builder.verbosity */
81+
fun verbosity(verbosity: Optional<ResponseTextConfig.Verbosity>) = apply {
82+
configBuilder.verbosity(verbosity)
83+
}
84+
85+
/** @see ResponseTextConfig.Builder.verbosity */
86+
fun verbosity(verbosity: JsonField<ResponseTextConfig.Verbosity>) = apply {
87+
configBuilder.verbosity(verbosity)
88+
}
89+
90+
/** @see ResponseTextConfig.Builder.additionalProperties */
91+
fun additionalProperties(additionalProperties: Map<String, JsonValue>) = apply {
92+
configBuilder.additionalProperties(additionalProperties)
93+
}
94+
95+
/** @see ResponseTextConfig.Builder.putAdditionalProperty */
96+
fun putAdditionalProperty(key: String, value: JsonValue) = apply {
97+
configBuilder.putAdditionalProperty(key, value)
98+
}
99+
100+
/** @see ResponseTextConfig.Builder.putAllAdditionalProperties */
101+
fun putAllAdditionalProperties(additionalProperties: Map<String, JsonValue>) = apply {
102+
configBuilder.putAllAdditionalProperties(additionalProperties)
103+
}
104+
105+
/** @see ResponseTextConfig.Builder.removeAdditionalProperty */
106+
fun removeAdditionalProperty(key: String) = apply {
107+
configBuilder.removeAdditionalProperty(key)
108+
}
109+
110+
/** @see ResponseTextConfig.Builder.removeAllAdditionalProperties */
111+
fun removeAllAdditionalProperties(keys: Set<String>) = apply {
112+
configBuilder.removeAllAdditionalProperties(keys)
113+
}
114+
115+
/**
116+
* Returns an immutable instance of [StructuredResponseTextConfig].
117+
*
118+
* Further updates to this [Builder] will not mutate the returned instance.
119+
*/
120+
fun build(): StructuredResponseTextConfig<T> =
121+
StructuredResponseTextConfig<T>(
122+
checkRequired("responseType", responseType),
123+
configBuilder.build(),
124+
)
125+
}
126+
127+
override fun equals(other: Any?): Boolean {
128+
if (this === other) {
129+
return true
130+
}
131+
132+
return other is StructuredResponseTextConfig<*> &&
133+
responseType == other.responseType &&
134+
rawConfig == other.rawConfig
135+
}
136+
137+
private val hashCode: Int by lazy { Objects.hash(responseType, rawConfig) }
138+
139+
override fun hashCode(): Int = hashCode
140+
141+
override fun toString() =
142+
"${javaClass.simpleName}{responseType=$responseType, rawConfig=$rawConfig}"
143+
}

openai-java-core/src/test/kotlin/com/openai/core/StructuredOutputsTestUtils.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,14 +321,15 @@ internal fun checkAllDelegatorReadFunctionsAreTested(
321321
* @param delegationTestCases The tests cases that identify the names of delegating functions for
322322
* which parameterized unit tests have been defined.
323323
* @param exceptionalTestedFns The names of delegating functions that are tested separately, not as
324-
* parameterized unit tests. This is usually because they require special handling in the test.
324+
* parameterized unit tests. This is usually because they require special handling in the test. If
325+
* functions are overloaded, repeat the name for of the function for each overload.
325326
* @param nonDelegatingFns The names of functions that do not perform any delegation and for which
326327
* delegation tests are not required.
327328
*/
328329
internal fun checkAllDelegatorWriteFunctionsAreTested(
329330
delegatorClass: KClass<*>,
330331
delegationTestCases: List<DelegationWriteTestCase>,
331-
exceptionalTestedFns: Set<String>,
332+
exceptionalTestedFns: List<String>,
332333
nonDelegatingFns: Set<String>,
333334
) {
334335
// There are exceptional test cases for some functions. Most other functions are part of the

0 commit comments

Comments
 (0)