Skip to content

fix(client): support structured outputs in async requests #550

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 1 commit into from
Jul 22, 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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import com.openai.models.responses.ResponseCreateParams
import com.openai.models.responses.ResponseDeleteParams
import com.openai.models.responses.ResponseRetrieveParams
import com.openai.models.responses.ResponseStreamEvent
import com.openai.models.responses.StructuredResponse
import com.openai.models.responses.StructuredResponseCreateParams
import com.openai.services.async.responses.InputItemServiceAsync
import java.util.concurrent.CompletableFuture
import java.util.function.Consumer
Expand Down Expand Up @@ -63,6 +65,30 @@ interface ResponseServiceAsync {
fun create(requestOptions: RequestOptions): CompletableFuture<Response> =
create(ResponseCreateParams.none(), requestOptions)

/**
* Creates a model response. The model's structured output in JSON form will be deserialized
* automatically into an instance of the class `T`. See the SDK documentation for more details.
*
* @see create
*/
fun <T : Any> create(
params: StructuredResponseCreateParams<T>
): CompletableFuture<StructuredResponse<T>> = create(params, RequestOptions.none())

/**
* Creates a model response. The model's structured output in JSON form will be deserialized
* automatically into an instance of the class `T`. See the SDK documentation for more details.
*
* @see create
*/
fun <T : Any> create(
params: StructuredResponseCreateParams<T>,
requestOptions: RequestOptions = RequestOptions.none(),
): CompletableFuture<StructuredResponse<T>> =
create(params.rawParams, requestOptions).thenApply {
StructuredResponse<T>(params.responseType, it)
}

/**
* Creates a model response. Provide [text](https://platform.openai.com/docs/guides/text) or
* [image](https://platform.openai.com/docs/guides/images) inputs to generate
Expand Down Expand Up @@ -92,6 +118,26 @@ interface ResponseServiceAsync {
fun createStreaming(requestOptions: RequestOptions): AsyncStreamResponse<ResponseStreamEvent> =
createStreaming(ResponseCreateParams.none(), requestOptions)

/**
* Creates a streaming model response for the given response conversation. The input parameters
* can define a JSON schema derived automatically from an arbitrary class to request a
* structured output in JSON form. However, that structured output is split over multiple
* streamed events, so it will not be deserialized automatically into an instance of that class.
* To deserialize the output, first use a helper class to accumulate the stream of events into a
* single output value. See the
* [SDK documentation](https://github.com/openai/openai-java/#usage-with-streaming) for full
* details.
*/
fun createStreaming(
params: StructuredResponseCreateParams<*>
): AsyncStreamResponse<ResponseStreamEvent> = createStreaming(params, RequestOptions.none())

/** @see [createStreaming] */
fun createStreaming(
params: StructuredResponseCreateParams<*>,
requestOptions: RequestOptions = RequestOptions.none(),
): AsyncStreamResponse<ResponseStreamEvent> = createStreaming(params.rawParams, requestOptions)

/** Retrieves a model response with the given ID. */
fun retrieve(responseId: String): CompletableFuture<Response> =
retrieve(responseId, ResponseRetrieveParams.none())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import com.openai.models.chat.completions.ChatCompletionListPageAsync
import com.openai.models.chat.completions.ChatCompletionListParams
import com.openai.models.chat.completions.ChatCompletionRetrieveParams
import com.openai.models.chat.completions.ChatCompletionUpdateParams
import com.openai.models.chat.completions.StructuredChatCompletion
import com.openai.models.chat.completions.StructuredChatCompletionCreateParams
import com.openai.services.async.chat.completions.MessageServiceAsync
import java.util.concurrent.CompletableFuture
import java.util.function.Consumer
Expand Down Expand Up @@ -64,6 +66,32 @@ interface ChatCompletionServiceAsync {
requestOptions: RequestOptions = RequestOptions.none(),
): CompletableFuture<ChatCompletion>

/**
* Creates a model response for the given chat conversation. The model's structured output in
* JSON form will be deserialized automatically into an instance of the class `T`. See the SDK
* documentation for more details.
*
* @see create
*/
fun <T : Any> create(
params: StructuredChatCompletionCreateParams<T>
): CompletableFuture<StructuredChatCompletion<T>> = create(params, RequestOptions.none())

/**
* Creates a model response for the given chat conversation. The model's structured output in
* JSON form will be deserialized automatically into an instance of the class `T`. See the SDK
* documentation for more details.
*
* @see create
*/
fun <T : Any> create(
params: StructuredChatCompletionCreateParams<T>,
requestOptions: RequestOptions = RequestOptions.none(),
): CompletableFuture<StructuredChatCompletion<T>> =
create(params.rawParams, requestOptions).thenApply {
StructuredChatCompletion<T>(params.responseType, it)
}

/**
* **Starting a new project?** We recommend trying
* [Responses](https://platform.openai.com/docs/api-reference/responses) to take advantage of
Expand Down Expand Up @@ -92,6 +120,26 @@ interface ChatCompletionServiceAsync {
requestOptions: RequestOptions = RequestOptions.none(),
): AsyncStreamResponse<ChatCompletionChunk>

/**
* Creates a streaming model response for the given chat conversation. The input parameters can
* define a JSON schema derived automatically from an arbitrary class to request a structured
* output in JSON form. However, that structured output is split over multiple streamed events,
* so it will not be deserialized automatically into an instance of that class. To deserialize
* the output, first use a helper class to accumulate the stream of events into a single output
* value. See the
* [SDK documentation](https://github.com/openai/openai-java/#usage-with-streaming) for full
* details.
*/
fun createStreaming(
params: StructuredChatCompletionCreateParams<*>
): AsyncStreamResponse<ChatCompletionChunk> = createStreaming(params, RequestOptions.none())

/** @see [createStreaming] */
fun createStreaming(
params: StructuredChatCompletionCreateParams<*>,
requestOptions: RequestOptions = RequestOptions.none(),
): AsyncStreamResponse<ChatCompletionChunk> = createStreaming(params.rawParams, requestOptions)

/**
* Get a stored chat completion. Only Chat Completions that have been created with the `store`
* parameter set to `true` will be returned.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,10 @@ interface ResponseService {
* can define a JSON schema derived automatically from an arbitrary class to request a
* structured output in JSON form. However, that structured output is split over multiple
* streamed events, so it will not be deserialized automatically into an instance of that class.
* See the [SDK documentation](https://github.com/openai/openai-java/#usage-with-streaming) for
* full details.
* To deserialize the output, first use a helper class to accumulate the stream of events into a
* single output value. See the
* [SDK documentation](https://github.com/openai/openai-java/#usage-with-streaming) for full
* details.
*/
@MustBeClosed
fun createStreaming(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.openai.example;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.openai.client.OpenAIClientAsync;
import com.openai.client.okhttp.OpenAIOkHttpClientAsync;
import com.openai.helpers.ResponseAccumulator;
import com.openai.models.ChatModel;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.StructuredResponseCreateParams;
import java.util.List;

public final class ResponsesStructuredOutputsStreamingAsyncExample {

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<Book> books;
}

private ResponsesStructuredOutputsStreamingAsyncExample() {}

public static void main(String[] args) {
// Configure using one of:
// - The `OPENAI_API_KEY` environment variable
// - The `OPENAI_BASE_URL` and `AZURE_OPENAI_KEY` environment variables
OpenAIClientAsync client = OpenAIOkHttpClientAsync.fromEnv();

StructuredResponseCreateParams<BookList> createParams = ResponseCreateParams.builder()
.input("List some famous late twentieth century novels.")
.text(BookList.class)
.model(ChatModel.GPT_4O)
.build();

ResponseAccumulator accumulator = ResponseAccumulator.create();

client.responses()
.createStreaming(createParams)
.subscribe(event -> accumulator
.accumulate(event)
.outputTextDelta()
.ifPresent(textEvent -> System.out.print(textEvent.delta())))
.onCompleteFuture()
.join();
System.out.println();

accumulator.response(BookList.class).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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.openai.example;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.openai.client.OpenAIClientAsync;
import com.openai.client.okhttp.OpenAIOkHttpClientAsync;
import com.openai.models.ChatModel;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.openai.models.chat.completions.StructuredChatCompletionCreateParams;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

public final class StructuredOutputsAsyncExample {

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.")
@Schema(minimum = "1500")
public int publicationYear;

public String genre;

@JsonIgnore
public String isbn;

@Override
public String toString() {
return '"' + title + "\" (" + publicationYear + ") [" + genre + "] by " + author;
}
}

public static class BookList {
@ArraySchema(maxItems = 100)
public List<Book> books;
}

private StructuredOutputsAsyncExample() {}

public static void main(String[] args) {
// Configure using one of:
// - The `OPENAI_API_KEY` environment variable
// - The `OPENAI_BASE_URL` and `AZURE_OPENAI_KEY` environment variables
OpenAIClientAsync client = OpenAIOkHttpClientAsync.fromEnv();

StructuredChatCompletionCreateParams<BookList> createParams = ChatCompletionCreateParams.builder()
.model(ChatModel.GPT_4O_MINI)
.maxCompletionTokens(2048)
.responseFormat(BookList.class)
.addUserMessage("List some famous late twentieth century novels.")
.build();

client.chat()
.completions()
.create(createParams)
.thenAccept(completion -> completion.choices().stream()
.flatMap(choice -> choice.message().content().stream())
.flatMap(bookList -> bookList.books.stream())
.forEach(book -> System.out.println(" - " + book)))
.join();
}
}
Loading