Spring AI 2.1 Message Parts: Reasoning That Survives the Round Trip

Engineering | Dimitar Proynov | October 28, 2026 | 10 min read | ...

Spring AI: Message Parts Doc | Upgrade Notes | OpenAI Responses Doc | Session Doc

Reasoning models do more than think before they answer. They hand part of that thinking back to you and expect to get it back on the next request. Claude signs its thinking blocks. Gemini attaches thought signatures to its function calls. OpenAI's Responses API returns encrypted reasoning items. If you drop, reorder or edit them, the result ranges from an HTTP 400 to a model that quietly loses its earlier reasoning and starts over.

Until now, a Spring AI message was a piece of text with side lists of tool calls and media. There was no place for that data, let alone its order. Spring AI 2.1 changes the data model itself: every message now holds its content as an ordered list of message parts. A part is one piece of a response, such as a run of text, a block of reasoning or a tool call. Providers call these pieces content blocks (Anthropic, Bedrock), parts (Gemini), items (OpenAI Responses) or chunks (Mistral), and Spring AI maps them all to message parts.

With the 2.1.0-RC1 release candidate, the major chat models produce and consume parts: OpenAI Responses, Anthropic, Google GenAI, OpenAI Chat Completions, Ollama, Bedrock Converse, DeepSeek and Mistral AI. The release also adds Session memory, which persists them. This post covers the new abstraction, why the Responses API needed it, and what each chat model gains. It comes down to three rules: keep the order, replay what you were given verbatim, and persist the parts, not the text.

Why a String Was Not Enough

Here is what Claude, with thinking enabled, returns in a single tool-calling round:

thinking   "The user wants the weather in Paris. I should call get_weather..."   signature: "EqQBCgIYAhIM..."
text       "Let me check that for you."
tool_use   get_weather {"location": "Paris"}

When you send the tool result, Anthropic wants this turn back exactly as it was: same blocks, same order, signature included. Before 2.1, AssistantMessage could not do that:

  1. No order across kinds. Text, tool calls and media lived in separate fields, so replay had to guess an order.
  2. No home for opaque data. Signatures and encrypted reasoning are opaque: the client cannot read them and must send them back unchanged. They sat under provider-specific metadata keys or in package-private subclasses.
  3. Streaming lost it. The aggregator merged chunk metadata key by key, so thinking often survived only as its last fragment.
  4. Memory lost it. JdbcChatMemoryRepository stores only the text and the message type.

The MessagePart Abstraction

AbstractMessage now holds a parts: List<MessagePart>, shared by every message type:

Spring AI Message API: AbstractMessage aggregates an ordered list of MessagePart entries

MessagePart is a sealed interface with six record implementations:

Part Holds Used for
TextPart a run of text answers, prompts
ReasoningPart reasoning text, an optional summary, an optional payload Claude thinking, Gemini thoughts, DeepSeek reasoning_content
ToolCallPart an AssistantMessage.ToolCall, an optional payload function calls
ToolResultPart a tool result whose content is a list of other MessageParts tool results carrying more than text
MediaPart a Media: image, audio, video, document multimodal input and output
UnknownPart the raw JSON of a block Spring AI does not model server tool calls, citations, code execution
Spring AI MessagePart API: the sealed MessagePart interface, its six record implementations and the optional OpaquePayload

Adding MessagePart achieves:

  • Order is data. Parts keep the order the provider produced them in, so a thinking block stays in front of the tool call it led to.
  • Opaque payloads are replayed, never interpreted. A part can carry an OpaquePayload(provider, kind, data), such as an Anthropic signature, a Gemini thought_signature or an OpenAI encrypted_content. The ChatModel that produced it sends it back unchanged, and every other chat model skips it. This keeps a history safe to move between providers.
  • Nothing is dropped silently. A block a ChatModel does not understand becomes an UnknownPart and is replayed as-is to the provider that produced it.

The change is backward compatible. Constructors and builder methods keep working and produce parts in the order 2.0 used: text, tool calls, media. getText(), getToolCalls() and getMedia() are now views over the parts, next to the new getReasoning(), and getParts() returns everything in order. Code that reads reasoning is what most likely needs an update, since each chat model exposed it differently before 2.1:

ChatResponse response = chatModel.call(prompt);

// Before 2.1: each chat model exposed reasoning its own way
List<String> thoughts = response.getResults().stream()              // Google GenAI
    .filter(g -> Boolean.TRUE.equals(g.getOutput().getMetadata().get("isThought")))
    .map(g -> g.getOutput().getText())
    .toList();
String reasoning = ((DeepSeekAssistantMessage) response.getResult().getOutput())
    .getReasoningContent();                                          // DeepSeek

// 2.1: one accessor for every chat model
List<String> thoughts = response.getResult().getOutput().getReasoning().stream()
    .map(ReasoningPart::text)
    .toList();
String answer = response.getResult().getOutput().getText();          // unchanged

The upgrade notes list the remaining changes per chat model. When streaming, each part is stamped with its content block index and a partial flag, and MessageAggregator merges the deltas. The aggregated message equals the one a blocking call returns, signature included.

How Message Parts Made the Responses API Possible

Spring AI implements two OpenAI chat model endpoints. OpenAiChatModel uses Chat Completions, OpenAI's original chat API, which many other servers also implement. OpenAiResponsesChatModel, new in 2.1, uses the Responses API, OpenAI's newer API for reasoning models and agents, in its stateless mode.

The Responses API does not return a message. Each call returns an output array of items: reasoning, function_call and message, plus items for the tools OpenAI runs itself, such as web search. Each reasoning item carries its own id and encrypted_content.

OpenAiResponsesChatModel is stateless by design: it does not keep the conversation on OpenAI's side, but replays all of it on every call, so memory, advisors and RAG work exactly as they do with OpenAiChatModel. For a reasoning model, the replay works well only if it includes the model's earlier reasoning. OpenAI's reasoning guide asks for the reasoning items, function calls and function call outputs since the last user message.

Before 2.1, AssistantMessage could not hold these items faithfully:

  • It had no way to say "reasoning, then function call, then message".
  • It could not attach data to a single item. Each reasoning item has an id and carries the model's reasoning in encrypted form (encrypted_content), and both have to be sent back with that item. A message had only one metadata map for the whole response.
  • It had nowhere to keep the items of tools OpenAI runs itself, such as web search, without faking them as local tool calls.

With parts, the mapping is one to one and keeps the order of the output array:

Responses API item Message part
reasoning ReasoningPart with the summary, OpaquePayload("openai.responses", "encrypted_content", …) and the item id
function_call ToolCallPart with the item id
message TextPart
image_generation_call MediaPart
web search, file search, code interpreter, MCP, any other item UnknownPart with the raw JSON

On the next request, the chat model rebuilds the input items from the parts in the same order. That replay is worth having. OpenAI's now archived cookbook on better performance from reasoning models reports that including reasoning items "led to about a 3% improvement" on SWE-bench. Responses is also the endpoint for agents on current OpenAI models. Starting with GPT-5.4, Chat Completions does not support tool calling with a reasoning effort other than none.

What Each Chat Model Gains

Every other chat model follows the same pattern: map each response block to one part, rebuild the request from the parts in order, and replay only its own payloads. What that buys depends on what each provider demands:

Chat model Now replayed Improvement
Anthropic signed and redacted thinking, in block order; server tool blocks tool turns Claude accepts, reasoning continuity, cache hits
Google GenAI each thought signature on the part that carried it no errors on Gemini 3+ function calls, full reasoning quality on other parts
OpenAI Chat Completions reasoning_content continuity on vLLM, gpt-oss, Kimi and other compatible servers
Ollama the thinking field continuity in gpt-oss and qwen3 tool loops
Bedrock Converse signed, redacted and unsigned reasoning, also when streaming streaming tool loops with extended thinking work
DeepSeek reasoning_content of streamed and restored messages no errors in thinking mode with tools
Mistral AI thinking chunks, in chunk order the output quality Mistral says is lost without them

Anthropic

Within a tool-use turn, Claude requires its thinking blocks back, and you "can't rearrange, edit, or partially drop them": modified blocks are rejected with an HTTP 400. Before 2.1, Spring AI rebuilt the turn as text, then thinking, then tool use, whatever order Claude had used. It also dropped server tool blocks, and the streamed thinking never fully reached the aggregated message. Now every block is one part, replayed in order with its signature. Anthropic names two more gains: preserved thinking "lets Claude continue reasoning from where it left off", and preserved thinking blocks "enable cache hits during tool use". Claude Opus 4.5 and later, and Sonnet 4.6 and later, also keep earlier turns' thinking in context, which is where Session memory comes in.

Google GenAI

Google's thought signatures guide says to return each signature "in the exact part where it was received". On Gemini 3 and later, a missing signature on a function call fails the request "with a 400 error". On other parts, "performance may degrade". Spring AI previously collected all signatures in one list and reattached them to function calls by position. It also dropped the thoughts and text in front of a function call from the tool loop. Now each signature travels on its own part, and code execution and server-side tool parts are sent back as received.

OpenAI Chat Completions

OpenAI's own Chat Completions API returns no reasoning text, so Responses should be used for OpenAI reasoning models. OpenAiChatModel serves the compatible servers that do return reasoning_content, such as vLLM, gpt-oss deployments and Kimi. OpenAI's harmony guide asks you to pass gpt-oss's chain of thought back during tool calling. That replay already existed in 2.0.x, through a metadata key. It now rides on parts, so it survives persistence. Reasoning signed by another provider is never forwarded, and for servers that reject the field there is replayReasoningContent(false).

Ollama

Before 2.1, thinking lived only in response metadata and was never sent back. When streaming, only its last fragment survived. Now it is replayed in the assistant message's thinking field, as Ollama's tool calling docs ask: "return those fields together with any tool results in the follow-up request". The gain depends on the model. The gpt-oss and qwen3 templates use the reasoning of the current tool loop.

Bedrock Converse

Bedrock applies the Claude rule: "During tool use, you must pass thinking blocks back to the API for the last assistant message". BedrockProxyChatModel previously dropped reasoning from streamed responses. Now a response is a single generation of parts in block order, streams carry the reasoning, the signature and redacted reasoning, and unsigned reasoning from gpt-oss is replayed too.

DeepSeek

In thinking mode, a request that carries tools must pass back the reasoning_content of earlier assistant messages, or the API returns a 400. Before 2.1, Spring AI sent it only for DeepSeekAssistantMessage instances, so streamed responses and messages restored from memory went out without it. Now any assistant message replays its ReasoningParts, streamed or not, and DeepSeekAssistantMessage is deprecated.

Mistral AI

Mistral's reasoning docs say: "always replay the full assistant message (including ThinkChunk)". They warn that dropping the reasoning trace "degrades model performance". Before 2.1, MistralAiChatModel never sent thinking back. Now thinking, text, citation and image chunks become parts and are replayed in chunk order, including between the rounds of a tool loop.

Memory That Keeps the Parts: Spring AI Session

Replay inside one ChatClient call is half the story. Between calls, the history lives in chat memory, and of the ChatMemory repositories only the in-memory one keeps the parts. With any other, the model reasons from scratch on each restored turn.

Spring AI Session, now part of Spring AI 2.1, closes that gap. A session is an append-only event log. Each message is stored as its full list of parts, payloads and attributes included, and the JDBC store keeps them as JSON. JdbcChatMemoryRepository, the JDBC store behind ChatMemory, keeps only the text and drops tool calls and tool results entirely. Two more properties matter for reasoning models:

  • Every tool round is recorded. SessionMemoryAdvisor runs inside the tool-calling loop, so it stores each round's tool calls and results, not just the final answer. Each round's prompt is rebuilt from the session, so signatures make the round trip through the store.
  • Compaction respects turns. When the history is trimmed or summarized, the cut always lands on a turn boundary. A tool call never loses its result, and the reasoning in front of it stays with it.

Add the spring-ai-starter-model-session-jdbc starter, register the session advisors and pass a session id on each call:

@Bean
ChatClient chatClient(ChatClient.Builder builder, SessionService sessionService) {
    return builder
        .defaultAdvisors(SessionAdvisors.builder(sessionService)
            .compaction(new TurnCountTrigger(20),
                    WindowCompactionStrategy.builder().lastTurns(10).build())
            .build())
        .build();
}

String answer = chatClient.prompt()
    .user("Find the cheapest flight to Lisbon next Friday")
    .advisors(SessionAdvisors.session("session-42"))
    .call()
    .content();

With this in place, the gains above carry across calls and restarts. Claude still sees its earlier thinking, Gemini gets each signature back on the right part, and DeepSeek's thinking-mode rule holds after a redeploy. ChatMemory and its repositories are deprecated in 2.1 and will be removed in 3.0.0. The migration guide maps one API onto the other.

⚠️ Things to know

One generation per response. Before 2.1, Anthropic, Google GenAI and Bedrock Converse split a response into separate Generations for thinking, tool calls and text. They now return one per candidate. Read reasoning with getResult().getOutput().getReasoning().

Histories stored before 2.1 have no reasoning parts. Messages that an earlier version wrote to chat memory come back without their reasoning, so it is not replayed.

Cross-provider history is safe, not lossless. Reasoning signed by one provider is never sent to another. When you switch models mid-conversation, the text and tool calls carry over, but the other vendor's reasoning does not.

Keep part attributes. Mistral, and Bedrock for unsigned reasoning, replay reasoning only when the part's provider attribute names them. If you store history yourself, don't strip attributes.

Conclusion

Message parts turn a Spring AI message from a string with attachments into a faithful record of what the model produced and in what order. The key insights:

  • Order is part of the data. Reasoning models check the order and integrity of what you send back. A string with side lists can carry neither.
  • Replay what you were given, verbatim. OpaquePayload lets each chat model return its own signatures unchanged and skip everyone else's.
  • Persist the parts, not the text. Session memory stores every part of every tool round, so reasoning survives across calls and restarts.

Resources

Spring AI

Related Spring AI articles

Get the Spring newsletter

Stay connected with the Spring newsletter

Subscribe

Get ahead

VMware offers training and certification to turbo-charge your progress.

Learn more

Get support

TrueSource Spring Enterprise offers support and binaries for OpenJDK™, Spring, and Apache Tomcat® in one simple subscription.

Learn more

Upcoming events

Check out all the upcoming events in the Spring community.

View all