|
This version is still in development and is not considered stable yet. For the latest stable version, please use Spring AI 2.0.2! |
System One Decision Models
Spring AI supports the System One protocol 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.
System One is the POST /v1/systemone protocol of the Jev decision models of TypeSafe.
Laya, llmman and Ollama serve it too, so one implementation works with all four.
SystemOneDecisionModel is built on the TypeSafeClient of the TypeSafe Java SDK.
Prerequisites
-
A System One server, for example:
-
TypeSafe: create an account and an API key at typesafe.ai;
-
Ollama 0.35 or later, with a decision model such as
d1-omni:600m. We recommend Ollama 0.40.4 or later; -
Laya, with
laya-serve; -
llmman 0.1.497 or later, with
llmman serve.
-
-
The
spring-ai-systemonedependency in your project, as described in Manual Configuration.
Manual Configuration
Spring Boot auto-configuration is not available for this model yet, so you configure the SystemOneDecisionModel yourself.
Add the spring-ai-systemone dependency to your Maven pom.xml or Gradle build.gradle build files:
-
Maven
-
Gradle
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-systemone</artifactId>
</dependency>
dependencies {
implementation 'org.springframework.ai:spring-ai-systemone'
}
| Refer to the Dependency Management section to add the Spring AI BOM to your build file. |
The TypeSafeClient holds the connection settings: the base URL, the API key, the default model, the timeout of each attempt and the retry policy.
The model takes the client and, optionally, the default options and the provider name for observations.
TypeSafeClient client = TypeSafeClient.builder()
.apiKey(System.getenv("TYPESAFE_API_KEY"))
.defaultModel("jev-latest")
.timeout(Duration.ofSeconds(10))
.build();
SystemOneDecisionModel decisionModel = SystemOneDecisionModel.builder(client).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")));
When you call TypeSafeClient.builder().build() without settings, the client reads the TYPESAFE_API_KEY, TYPESAFE_BASE_URL and TYPESAFE_DEFAULT_MODEL environment variables.
The official TypeSafe SDKs read the same variables.
| Builder method | Description | Default |
|---|---|---|
|
The default options. When they set no model, the default model of the client is used. |
the default model of the client |
|
The provider name for observations, such as |
|
|
The registry that observes the calls. |
|
|
The convention that the observations follow. |
the default convention |
Backends
All backends use the same protocol. They differ in where the client points to, and in what the response leaves out.
| Backend | Client | Model | Differences |
|---|---|---|---|
TypeSafe Jev |
|
|
The reference implementation. It reports a request id, which becomes the response id. |
Ollama 0.35+ |
|
A model that you pulled, such as |
No request id. It rejects a yes/no question without text. It can answer differently when the order of the fields changes. It loads the model on the first call, so give that call a long timeout. |
Laya 0.3.7+ |
|
The checkpoint names of Laya, such as |
No request id.
Output tokens are always |
llmman 0.1.497+ |
|
A local GGUF model, such as |
No request id.
Output tokens are always |
For Ollama, the OllamaDecisionModel uses the same protocol, and adds Spring Boot auto-configuration and the pulling of models.
|
The backends use different models, so their answers differ. When you move from one backend to another, check your thresholds again.
Set the provider name to tell the backends apart in observations:
TypeSafeClient ollama = TypeSafeClient.builder()
.baseUrl("http://localhost:11434")
.apiKey("ollama")
.defaultModel("d1-omni:600m")
.timeout(Duration.ofSeconds(60))
.retryPolicy(RetryPolicy.noRetry())
.build();
SystemOneDecisionModel decisionModel = SystemOneDecisionModel.builder(ollama)
.provider("ollama")
.build();
Runtime Options
SystemOneDecisionOptions holds the model name, which is the only setting that the protocol takes for each request.
You can change it for one call by setting options on the prompt:
DecisionResponse response = decisionModel.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(SystemOneDecisionOptions.builder()
.model("jev-preview")
.build())
.build());
boolean defect = response.getYesNo("defect").isYes();
String topic = response.getChoice("topic").getValue();
int satisfaction = response.getScore("satisfaction").getNearestLevel();
The portable DecisionOptions work too.
The timeout, the retry policy and the headers are settings of the TypeSafeClient, and they apply to every call.
Supported Features
| Feature | Supported | Notes |
|---|---|---|
Text input |
Yes |
|
Text with images |
No |
System One decides about text only, so an image in the input makes the call fail. |
Fields input |
Yes |
Use a |
Conversation input |
Yes |
Tool calls and tool results are sent, reasoning is not. |
Probabilities |
Yes |
For all three kinds of questions. |
Confidence |
Yes |
For choice and score answers.
A confidence of |
Refusals |
No |
|
Response id |
Depends on the backend |
Only TypeSafe Jev reports one. See Backends. |
Request Mapping
Each question becomes one entry of the questions of the request, under the name of the question.
| Spring AI question | System One question | Notes |
|---|---|---|
|
|
When set, |
|
|
Each option becomes a criterion with the name of the option, and its description or |
|
|
Each level becomes a criterion, from the lowest to the highest: its label, or a |
The input becomes the state of the request:
-
for
DecisionInput.text(…), or an input made of text parts only: the text, as a string; -
for
DecisionInput.fields(…): a JSON object, with the fields in the order of the map. A field that holds content parts becomes their text; -
for
DecisionInput.conversation(…): a{"messages": […]}object, with one entry for each message that has something to send. Each entry has theroleof the message, its text ascontent, its tool calls astool_calls, and its tool results astool_results. The arguments of a tool call are sent as a nested object when they are JSON.
System One decides about text only.
A MediaPart anywhere in the input makes the call fail.
In a conversation, the reasoning parts and unknown parts of a message are not sent.
A message with nothing left to send, such as a message with only reasoning, is left out.
Response Mapping
A noul answer becomes a YesNoAnswer.
A choice answer becomes a ChoiceAnswer, with the probability of each option and the confidence.
A score answer becomes a ScoreAnswer, with the probability of each level and the confidence.
Spring AI checks the answers against the prompt. An answer for a question that the prompt didn’t ask, or an answer of the wrong kind, makes the call fail. An answer of a kind that the SDK doesn’t know makes the call fail too.
The SDK reports a confidence of 0 when the server sends none, so a confidence of 0 can mean that there is no confidence.
The model that answered and the token usage come from the response body.
When the server sends a request id in the x-typesafe-request-id header, Spring AI records it as the id of the response.
You get it from response.getMetadata().getId() and from the gen_ai.response.id observation key.
Ollama, Laya and llmman send no request id, so the id is null, and the observation has no such key.
Errors come as exceptions of the TypeSafeException hierarchy of the SDK, such as TypeSafeBadRequestException or TypeSafeRateLimitException.
They come after the retries of the RetryPolicy of the client.
Observability
Spring AI observes the calls as decision operations, with the configured provider name as the gen_ai.system key.
See Decision Model observability.