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

  1. 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.

  2. The spring-ai-systemone dependency 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

options(SystemOneDecisionOptions)

The default options. When they set no model, the default model of the client is used.

the default model of the client

provider(String)

The provider name for observations, such as ollama, laya or llmman when the client points at one of them.

systemone

observationRegistry(ObservationRegistry)

The registry that observes the calls.

ObservationRegistry.NOOP

observationConvention(DecisionModelObservationConvention)

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

baseUrl api.typesafe.ai (the default), and your API key

jev-latest (the default), or another TypeSafeModels constant

The reference implementation. It reports a request id, which becomes the response id.

Ollama 0.35+

baseUrl localhost:11434, and any API key that is not empty, such as ollama

A model that you pulled, such as d1-omni:600m. Ollama has no jev-latest, so set defaultModel(…​).

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+

baseUrl of your laya-serve, such as localhost:8002, and its LAYA_API_KEY when set

The checkpoint names of Laya, such as english. Laya routes other names automatically.

No request id. Output tokens are always 0. It serves one request at a time.

llmman 0.1.497+

baseUrl 127.0.0.1:17434 (the default of llmman serve), and one of its LLMMAN_API_KEYS when set, or else any API key that is not empty, such as llmman

A local GGUF model, such as gemma4. llmman has no jev-latest, so set defaultModel(…​).

No request id. Output tokens are always 0.

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 LinkedHashMap to keep the fields in order.

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 0 can mean that the server reported none.

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

YesNoQuestion

noul

When set, whenYes and whenNo are sent as the true and false criteria.

ChoiceQuestion

choice

Each option becomes a criterion with the name of the option, and its description or null.

ScoreQuestion

score

Each level becomes a criterion, from the lowest to the highest: its label, or a {"label", "description"} object when it has a description.

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 the role of the message, its text as content, its tool calls as tool_calls, and its tool results as tool_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.

Example Code

The SystemOneDecisionModelIT test runs against the server that the TYPESAFE_BASE_URL, TYPESAFE_API_KEY and TYPESAFE_DEFAULT_MODEL environment variables point to. It shows the three kinds of questions, and the fields and conversation inputs.