This version is still in development and is not considered stable yet. For the latest stable version, please use Spring AI 2.0.2!

OpenAI Decision Models

Spring AI supports the OpenAI Decisions API through the Decision Models API. A decision model picks one result from a known set of possible results, such as yes or no, one of a few categories, or a level on a scale, and tells you how likely each result is.

OpenAiDecisionModel uses the Decisions API, an endpoint that is separate from chat completions. By default, it uses the gpt-6-luna model.

Prerequisites

  1. Create an OpenAI account and get an API key. You can sign up on the OpenAI signup page and create an API key on the API Keys page.

  2. Add the Spring AI OpenAI starter to your project, as described in Auto-configuration.

Auto-configuration

Spring AI provides Spring Boot auto-configuration for the OpenAI decision model. To enable it, add the following dependency to your Maven pom.xml or Gradle build.gradle build files:

  • Maven

  • Gradle

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
dependencies {
    implementation 'org.springframework.ai:spring-ai-starter-model-openai'
}
Refer to the Dependency Management section to add the Spring AI BOM to your build file.

If your application doesn’t use the decision model, turn off its auto-configuration with spring.ai.model.decision=none, as described in Decision Properties.

Connection Properties

The prefix spring.ai.openai is the property prefix to configure the connection to OpenAI.

Property

Description

Default

spring.ai.openai.base-url

The URL to connect to.

https://api.openai.com

spring.ai.openai.api-key

The API key.

-

spring.ai.openai.organization-id

The organization to use for the requests, if any.

-

spring.ai.openai.timeout

The request timeout of the OpenAI client.

60s

spring.ai.openai.max-retries

The maximum number of retries of the OpenAI client.

3

spring.ai.openai.proxy

The proxy settings of the OpenAI client.

-

spring.ai.openai.custom-headers

Custom HTTP headers to add to the requests of the OpenAI client.

empty

spring.ai.openai.connection-pool-metrics-enabled

Whether to enable connection pool metrics for the OkHttp client.

false

Decision Properties

The spring.ai.model.decision property enables and disables the decision auto-configuration.

To enable it, set spring.ai.model.decision=openai (this is the default).

To disable it, set spring.ai.model.decision=none, or any value other than openai.

The prefix spring.ai.openai.decision is the property prefix to configure the OpenAI decision model.

Property

Description

Default

spring.ai.model.decision

Enables the OpenAI decision model.

openai

spring.ai.openai.decision.base-url

The URL to connect to.

https://api.openai.com

spring.ai.openai.decision.api-key

The API key.

-

spring.ai.openai.decision.organization-id

The organization to use for the requests, if any.

-

spring.ai.openai.decision.model

The name of the decision model to use.

gpt-6-luna

spring.ai.openai.decision.safety-identifier

A stable identifier of the end user, sent to OpenAI to help detect abuse.

-

The spring.ai.openai.decision.base-url, spring.ai.openai.decision.api-key and spring.ai.openai.decision.organization-id properties, when set, take precedence over the common spring.ai.openai ones. This is useful when you use different OpenAI accounts for different models.
You can change the spring.ai.openai.decision.model and spring.ai.openai.decision.safety-identifier properties for one call, with the Runtime Options of the DecisionPrompt.

Runtime Options

OpenAiDecisionOptions holds the settings of a decision call. At startup, the model uses the settings of the spring.ai.openai.decision properties. You can change some of them for one call by setting options on the prompt:

DecisionResponse response = openAiDecisionModel.call(DecisionPrompt.builder()
    .text("My new headphones arrived yesterday, but the left side doesn't work. Can I get my money back?")
    .question("defect", YesNoQuestion.builder()
        .text("Does the customer report a defect?")
        .whenYes("Part of the item does not work as it should")
        .whenNo("The item works, and the message is about something else")
        .build())
    .question("topic", ChoiceQuestion.builder()
        .text("What is the message about?")
        .option("delivery", "Where an order is, or when it will arrive")
        .option("product", "A problem with an item the customer received")
        .option("account", "Login, address or payment details")
        .option("other", "None of the above")
        .build())
    .question("satisfaction", ScoreQuestion.of("How satisfied is the customer?", "unhappy", "neutral", "happy"))
    .options(OpenAiDecisionOptions.builder()
        .model("gpt-6-luna")
        .safetyIdentifier("user-12345")
        .build())
    .build());

boolean defect = response.getYesNo("defect").isYes();
String topic = response.getChoice("topic").getValue();
int satisfaction = response.getScore("satisfaction").getNearestLevel();

When you only change the model, the portable DecisionOptions work too.

On a prompt, you can set model, safetyIdentifier, timeout, customHeaders and deploymentName. The other settings, such as baseUrl, apiKey, organizationId, maxRetries and proxy, configure the client when the model is built, so they have no effect on a prompt.

deploymentName and the Microsoft Foundry and GitHub Models settings exist for consistency with the other OpenAI models. Check the documentation of those platforms to see whether they offer the Decisions API.

Supported Features

Feature Supported Notes

Text input

Yes

Text with images

Yes

Only images, and only as inline data, not as hosted URLs. See Request Mapping.

Fields input

Yes

Conversation input

Yes

Tool calls and tool results are sent as text, reasoning is not.

Probabilities

Yes

For all three kinds of questions.

Confidence

Yes

For choice and score answers.

Refusals

Yes

OpenAI can refuse to answer one question and still answer the others.

Response id

Yes

The request id of the x-request-id header.

Request Mapping

Each question becomes one entry of the questions of the request, under the name of the question.

Spring AI question OpenAI question type Notes

YesNoQuestion

predicate

The API has no field for criteria. When set, whenYes and whenNo are added to the instructions, as Answer "yes" if: …​ and Answer "no" if: …​ lines.

ChoiceQuestion

choice

Each option becomes a choice, with its value and description. OpenAI recommends a fallback option, such as other, when the options don’t cover every input.

ScoreQuestion

score

Each level becomes a level, with its label and description.

An input made of text only is sent as a string. Any other input is sent as one user message, made of these parts, in order:

  • for DecisionInput.content(…​): its content parts;

  • for DecisionInput.fields(…​): one text part for each field, as name: value, with maps and lists as JSON. A field that holds content parts becomes a name: text part, followed by those parts;

  • for DecisionInput.conversation(…​): the parts of each message, after the role of the message. Tool calls and tool results become extra text lines, and images become image parts where they appear.

Reasoning parts are not sent. The supported content parts are TextPart and MediaPart, and the only supported media are images.

The Decisions API accepts images only as inline base64 data URLs, not as hosted URLs. Give an image as bytes or as a Resource, for example MediaPart.of(new Media(MimeTypeUtils.IMAGE_PNG, new ClassPathResource("photo.png"))). A Media that holds a hosted URL is rejected.

Response Mapping

A predicate answer becomes a YesNoAnswer, a choice answer becomes a ChoiceAnswer, and a score answer becomes a ScoreAnswer. A refused question gets a RefusalAnswer.

The model that answered and the token usage come from the response body. The usage includes the cached and cache-write input tokens.

The Decisions API returns no id in the response body. Instead, Spring AI records the request id of the x-request-id header as the id of the response. You get it from response.getMetadata().getId() and from the gen_ai.response.id observation key. Use this id when you contact OpenAI about a decision.

Observability

Spring AI observes the calls as decision operations, with openai as the gen_ai.system key. See Decision Model observability.

Sample Controller

The starter creates a DecisionModel bean that you can inject into your classes. Here is a simple @RestController that uses it to sort customer messages:

@RestController
public class DecisionController {

    private final DecisionModel decisionModel;

    public DecisionController(DecisionModel decisionModel) {
        this.decisionModel = decisionModel;
    }

    @GetMapping("/ai/decision")
    public Map<String, Object> triage(@RequestParam(value = "message",
            defaultValue = "Where is my order? I ordered it two weeks ago.") String message) {
        DecisionResponse response = this.decisionModel.call(DecisionPrompt.builder()
            .text(message)
            .question("defect", YesNoQuestion.of("Does the customer report a defect?"))
            .question("topic", ChoiceQuestion.of("What is the message about?",
                    "delivery", "product", "account", "other"))
            .build());
        return Map.of("defect", response.getYesNo("defect").isYes(),
                "topic", response.getChoice("topic").getValue());
    }

}

Manual Configuration

If you don’t use Spring Boot, you can configure the OpenAiDecisionModel yourself. Add the spring-ai-openai dependency to your Maven pom.xml or Gradle build.gradle build files:

  • Maven

  • Gradle

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai</artifactId>
</dependency>
dependencies {
    implementation 'org.springframework.ai:spring-ai-openai'
}
Refer to the Dependency Management section to add the Spring AI BOM to your build file.

Then create an OpenAiDecisionModel, and use it to sort a customer message:

OpenAiDecisionModel decisionModel = OpenAiDecisionModel.builder()
    .options(OpenAiDecisionOptions.builder()
        .apiKey(System.getenv("OPENAI_API_KEY"))
        .model("gpt-6-luna")
        .build())
    .build();

DecisionResponse response = decisionModel.call("Where is my order? I ordered it two weeks ago.",
        Map.of("topic", ChoiceQuestion.of("What is the message about?",
                "delivery", "product", "account", "other")));

Customizing the HTTP Client

Spring AI uses the official openai-java SDK under the hood and configures its HTTP transport with a custom OkHttp client built by SpringAiOpenAiHttpClient.Builder. You can intercept that builder before the underlying OkHttpClient is created by exposing one or more OpenAiHttpClientBuilderCustomizer beans. Each customizer receives the same builder used by every OpenAI model (chat, embedding, image, audio, moderation), so the customization applies uniformly.

@FunctionalInterface
public interface OpenAiHttpClientBuilderCustomizer {
    void customize(SpringAiOpenAiHttpClient.Builder builder);
}

Typical use cases include:

  • registering OkHttp Interceptor instances (authentication, propagation headers, custom logging);

  • swapping the dispatcher ExecutorService (for example, to route async I/O through virtual threads);

  • configuring proxy, SSL, hostname verification, or the connection-pool sizing exposed by the builder.

When several customizers are present, they are applied in @Order / Ordered order, after Spring AI’s own defaults, so user code wins.

The same hook is available when wiring a model manually via the OpenAi*Model.Builder:

var chatModel = OpenAiChatModel.builder()
    .options(OpenAiChatOptions.builder().model("gpt-4o").build())
    .httpClientBuilderCustomizer(myCustomizer)
    .build();

Example Code

The OpenAiDecisionModelIT test has more examples of how to use the OpenAI decision model.