|
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
-
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.
-
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. |
|
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. |
|
spring.ai.openai.max-retries |
The maximum number of retries of the OpenAI client. |
|
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. |
|
Decision Properties
|
The To enable it, set To disable it, set |
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. |
|
spring.ai.openai.decision.base-url |
The URL to connect to. |
|
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. |
|
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 |
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 |
|---|---|---|
|
|
The API has no field for criteria.
When set, |
|
|
Each option becomes a choice, with its value and description.
OpenAI recommends a fallback option, such as |
|
|
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, asname: value, with maps and lists as JSON. A field that holds content parts becomes aname: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
Interceptorinstances (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();