Skip to content

Commit 1102ead

Browse files
damostainless-app[bot]
authored andcommitted
feat(client): support completions streaming structured outputs (#528)
* structured-streaming-495: updates, example and docs for streaming with structured outputs. * structured-streaming-495: improved accumulator API, partial support for Responses API. * structured-streaming-495: added links to SDK docs.
1 parent 972aa1a commit 1102ead

File tree

6 files changed

+181
-5
lines changed

6 files changed

+181
-5
lines changed

README.md

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,31 @@ the latter when `ResponseCreateParams.Builder.text(Class<T>)` is called.
508508
For a full example of the usage of _Structured Outputs_ with the Responses API, see
509509
[`ResponsesStructuredOutputsExample`](openai-java-example/src/main/java/com/openai/example/ResponsesStructuredOutputsExample.java).
510510

511+
### Usage with streaming
512+
513+
_Structured Outputs_ can also be used with [Streaming](#streaming) and the Chat Completions API. As
514+
responses are returned in "chunks", the full response must first be accumulated to concatenate the
515+
JSON strings that can then be converted into instances of the arbitrary Java class. Normal streaming
516+
operations can be performed while accumulating the JSON strings.
517+
518+
Use the [`ChatCompletionAccumulator`](openai-java-core/src/main/kotlin/com/openai/helpers/ChatCompletionAccumulator.kt)
519+
as described in the section on [Streaming helpers](#streaming-helpers) to accumulate the JSON
520+
strings. Once accumulated, use `ChatCompletionAccumulator.chatCompletion(Class<T>)` to convert the
521+
accumulated `ChatCompletion` into a
522+
[`StructuredChatCompletion`](openai-java-core/src/main/kotlin/com/openai/models/chat/completions/StructuredChatCompletion.kt).
523+
The `StructuredChatCompletion` can then automatically deserialize the JSON strings into instances of
524+
your Java class.
525+
526+
For a full example of the usage of _Structured Outputs_ with Streaming and the Chat Completions API,
527+
see
528+
[`StructuredOutputsStreamingExample`](openai-java-example/src/main/java/com/openai/example/StructuredOutputsStreamingExample.java).
529+
530+
At present, there is no accumulator for streaming responses using the Responses API. It is still
531+
possible to derive a JSON schema from a Java class and create a streaming response for a
532+
[`StructuredResponseCreateParams`](openai-java-core/src/main/kotlin/com/openai/models/responses/StructuredResponseCreateParams.kt)
533+
object, but there is no helper for deserialization of the response to an instance of that Java
534+
class.
535+
511536
### Defining JSON schema properties
512537

513538
When a JSON schema is derived from your Java classes, all properties represented by `public` fields
@@ -594,13 +619,13 @@ import io.swagger.v3.oas.annotations.media.ArraySchema;
594619
class Article {
595620
@ArraySchema(minItems = 1, maxItems = 10)
596621
public List<String> authors;
597-
622+
598623
@Schema(pattern = "^[A-Za-z ]+$")
599624
public String title;
600-
625+
601626
@Schema(format = "date")
602627
public String publicationDate;
603-
628+
604629
@Schema(minimum = "1")
605630
public int pageCount;
606631
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,8 @@ internal fun extractSchema(type: Class<*>): ObjectNode {
218218
}
219219

220220
/**
221-
* Creates an instance of a Java class using data from a JSON. The JSON data should conform to the
222-
* JSON schema previously extracted from the Java class.
221+
* Creates an instance of a Java class using data from a JSON string. The JSON data should conform
222+
* to the JSON schema previously extracted from the Java class.
223223
*
224224
* @throws OpenAIInvalidDataException If the JSON data cannot be parsed to an instance of the
225225
* [responseType] class.

openai-java-core/src/main/kotlin/com/openai/helpers/ChatCompletionAccumulator.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.openai.models.chat.completions.ChatCompletion
77
import com.openai.models.chat.completions.ChatCompletionChunk
88
import com.openai.models.chat.completions.ChatCompletionMessage
99
import com.openai.models.chat.completions.ChatCompletionMessageToolCall
10+
import com.openai.models.chat.completions.StructuredChatCompletion
1011
import java.util.Optional
1112
import kotlin.jvm.optionals.getOrNull
1213

@@ -122,6 +123,23 @@ class ChatCompletionAccumulator private constructor() {
122123
fun chatCompletion(): ChatCompletion =
123124
checkNotNull(chatCompletion) { "Final chat completion chunk(s) not yet received." }
124125

126+
/**
127+
* Gets the final accumulated chat completion with support for structured outputs. Until the
128+
* last chunk has been accumulated, a [StructuredChatCompletion] will not be available. Wait
129+
* until all chunks have been handled by [accumulate] before calling this method. See that
130+
* method for more details on how the last chunk is detected. See the SDK documentation on
131+
* _Structured Outputs_ for more details and example code.
132+
*
133+
* @param responseType The Java class from which the JSON schema in the request was derived. The
134+
* output JSON conforming to that schema can be converted automatically back to an instance of
135+
* that Java class by the [StructuredChatCompletion].
136+
* @throws IllegalStateException If called before the last chunk has been accumulated.
137+
* @throws OpenAIInvalidDataException If the JSON data cannot be parsed to an instance of the
138+
* [responseType] class.
139+
*/
140+
fun <T : Any> chatCompletion(responseType: Class<T>) =
141+
StructuredChatCompletion(responseType, chatCompletion())
142+
125143
/**
126144
* Accumulates a streamed chunk and uses it to construct a [ChatCompletion]. When all chunks
127145
* have been accumulated, the chat completion can be retrieved by calling [chatCompletion].

openai-java-core/src/main/kotlin/com/openai/services/blocking/ResponseService.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,26 @@ interface ResponseService {
116116
fun createStreaming(requestOptions: RequestOptions): StreamResponse<ResponseStreamEvent> =
117117
createStreaming(ResponseCreateParams.none(), requestOptions)
118118

119+
/**
120+
* Creates a streaming model response for the given response conversation. The input parameters
121+
* can define a JSON schema derived automatically from an arbitrary class to request a
122+
* structured output in JSON form. However, that structured output is split over multiple
123+
* streamed events, so it will not be deserialized automatically into an instance of that class.
124+
* See the [SDK documentation](https://github.com/openai/openai-java/#usage-with-streaming) for
125+
* full details.
126+
*/
127+
@MustBeClosed
128+
fun createStreaming(
129+
params: StructuredResponseCreateParams<*>
130+
): StreamResponse<ResponseStreamEvent> = createStreaming(params, RequestOptions.none())
131+
132+
/** @see [createStreaming] */
133+
@MustBeClosed
134+
fun createStreaming(
135+
params: StructuredResponseCreateParams<*>,
136+
requestOptions: RequestOptions = RequestOptions.none(),
137+
): StreamResponse<ResponseStreamEvent> = createStreaming(params.rawParams, requestOptions)
138+
119139
/** Retrieves a model response with the given ID. */
120140
fun retrieve(responseId: String): Response = retrieve(responseId, ResponseRetrieveParams.none())
121141

openai-java-core/src/main/kotlin/com/openai/services/blocking/chat/ChatCompletionService.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,28 @@ interface ChatCompletionService {
117117
requestOptions: RequestOptions = RequestOptions.none(),
118118
): StreamResponse<ChatCompletionChunk>
119119

120+
/**
121+
* Creates a streaming model response for the given chat conversation. The input parameters can
122+
* define a JSON schema derived automatically from an arbitrary class to request a structured
123+
* output in JSON form. However, that structured output is split over multiple streamed events,
124+
* so it will not be deserialized automatically into an instance of that class. To deserialize
125+
* the output, first use a helper class to accumulate the stream of events into a single output
126+
* value. See the
127+
* [SDK documentation](https://github.com/openai/openai-java/#usage-with-streaming) for full
128+
* details.
129+
*/
130+
@MustBeClosed
131+
fun createStreaming(
132+
params: StructuredChatCompletionCreateParams<*>
133+
): StreamResponse<ChatCompletionChunk> = createStreaming(params, RequestOptions.none())
134+
135+
/** @see [createStreaming] */
136+
@MustBeClosed
137+
fun createStreaming(
138+
params: StructuredChatCompletionCreateParams<*>,
139+
requestOptions: RequestOptions = RequestOptions.none(),
140+
): StreamResponse<ChatCompletionChunk> = createStreaming(params.rawParams, requestOptions)
141+
120142
/**
121143
* Get a stored chat completion. Only Chat Completions that have been created with the `store`
122144
* parameter set to `true` will be returned.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.openai.example;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnore;
4+
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
5+
import com.openai.client.OpenAIClient;
6+
import com.openai.client.okhttp.OpenAIOkHttpClient;
7+
import com.openai.core.http.StreamResponse;
8+
import com.openai.helpers.ChatCompletionAccumulator;
9+
import com.openai.models.ChatModel;
10+
import com.openai.models.chat.completions.ChatCompletionChunk;
11+
import com.openai.models.chat.completions.ChatCompletionCreateParams;
12+
import com.openai.models.chat.completions.StructuredChatCompletionCreateParams;
13+
import io.swagger.v3.oas.annotations.media.ArraySchema;
14+
import io.swagger.v3.oas.annotations.media.Schema;
15+
import java.util.List;
16+
17+
public final class StructuredOutputsStreamingExample {
18+
19+
public static class Person {
20+
@JsonPropertyDescription("The first name and surname of the person.")
21+
public String name;
22+
23+
public int birthYear;
24+
25+
@JsonPropertyDescription("The year the person died, or 'present' if the person is living.")
26+
public String deathYear;
27+
28+
@Override
29+
public String toString() {
30+
return name + " (" + birthYear + '-' + deathYear + ')';
31+
}
32+
}
33+
34+
public static class Book {
35+
public String title;
36+
37+
public Person author;
38+
39+
@JsonPropertyDescription("The year in which the book was first published.")
40+
@Schema(minimum = "1500")
41+
public int publicationYear;
42+
43+
public String genre;
44+
45+
@JsonIgnore
46+
public String isbn;
47+
48+
@Override
49+
public String toString() {
50+
return '"' + title + "\" (" + publicationYear + ") [" + genre + "] by " + author;
51+
}
52+
}
53+
54+
public static class BookList {
55+
@ArraySchema(maxItems = 100)
56+
public List<Book> books;
57+
}
58+
59+
private StructuredOutputsStreamingExample() {}
60+
61+
public static void main(String[] args) {
62+
// Configures using one of:
63+
// - The `OPENAI_API_KEY` environment variable
64+
// - The `OPENAI_BASE_URL` and `AZURE_OPENAI_KEY` environment variables
65+
OpenAIClient client = OpenAIOkHttpClient.fromEnv();
66+
67+
StructuredChatCompletionCreateParams<BookList> createParams = ChatCompletionCreateParams.builder()
68+
.model(ChatModel.GPT_4O_MINI)
69+
.maxCompletionTokens(2048)
70+
.responseFormat(BookList.class)
71+
.addUserMessage("List some famous late twentieth century novels.")
72+
.build();
73+
74+
ChatCompletionAccumulator accumulator = ChatCompletionAccumulator.create();
75+
76+
try (StreamResponse<ChatCompletionChunk> streamResponse =
77+
client.chat().completions().createStreaming(createParams)) {
78+
streamResponse.stream()
79+
.peek(accumulator::accumulate)
80+
.flatMap(completion -> completion.choices().stream())
81+
.flatMap(choice -> choice.delta().content().stream())
82+
.forEach(System.out::print);
83+
System.out.println();
84+
}
85+
86+
accumulator.chatCompletion(BookList.class).choices().stream()
87+
.flatMap(choice -> choice.message().content().stream())
88+
.flatMap(bookList -> bookList.books.stream())
89+
.forEach(book -> System.out.println(" - " + book));
90+
}
91+
}

0 commit comments

Comments
 (0)