Spring AI Streaming Source Code Analysis
Streaming is the de facto interaction mode for modern LLM-powered applications. Rather than waiting for the entire response to be generated, streaming delivers tokens to the client as they are produced, enabling real-time typing indicators, lower perceived latency, and the ability to abort long generations. Spring AI integrates streaming deeply into its architecture using Project Reactor, making it a first-class citizen alongside synchronous call().
This chapter examines the source code that powers streaming in Spring AI. We will trace a streaming request from the ChatClient fluent API down through the reactive ChatModel abstraction, provider-specific adapters, and the Flux<ChatResponse> pipeline. By the end, you will understand how the framework achieves non-blocking, backpressure-aware token delivery while remaining provider-agnostic.
What Is Spring AI Streaming?
Spring AI Streaming refers to the capability of requesting and consuming a large language model response as a stream of partial ChatResponse objects, each typically containing a single token or a small group of tokens. This contrasts with the synchronous call() method, which returns the complete response only after the entire generation is finished.
Key design goals:
- Incremental delivery – Emit each piece of the response as soon as the model produces it.
- Reactive programming – Leverage Project Reactor's
Fluxto provide a standard reactive-streams API. - Non-blocking I/O – Keep server threads free while waiting for model output.
- Unified API – The streaming contract is defined at the
ChatModellevel, so all providers can implement it uniformly.
Streaming dramatically improves user experience and enables scenarios like server‑sent events (SSE), real‑time dashboards, and early cancellation of expensive generations.
Streaming in Spring AI Architecture
Streaming is not bolted onto the framework; it is a parallel execution path that follows the same layered architecture as synchronous calls but uses reactive types.
- Application – Calls
chatClient.prompt().stream().chatResponse()to obtain aFlux<ChatResponse>. - ChatClient – Orchestrator; it builds the prompt, applies advisors, and then delegates to the
StreamingChatModel. - Prompt – Immutable request object, identical to the one used in synchronous calls.
- Advisor Chain – Pre‑processes the prompt; in streaming, response advisors are applied per‑emitted item or at the end.
- StreamingChatModel – An extension of
ChatModelthat declaresFlux<ChatResponse> stream(Prompt prompt). - Provider Adapter – Translates the prompt into a streaming HTTP request (using SSE or WebSocket) and maps each chunk to a
ChatResponse. - Flux – The reactive stream that the application subscribes to; it respects backpressure and supports cancellation.
The architecture ensures that the same advisors and tooling work for both streaming and non‑streaming paths, with minimal branching.
Core Interfaces and Classes
| Class / Interface | Responsibility |
|---|---|
ChatClient | Top‑level entry point; its response spec offers stream() to initiate streaming. |
StreamingChatModel | Interface extending ChatModel with Flux<ChatResponse> stream(Prompt prompt). |
DefaultChatClient | Implementation of ChatClient; internally decides whether to call call() or stream(). |
ChatResponse | Normalized response object; in streaming, each chunk is a ChatResponse containing a delta. |
Flux<ChatResponse> | Project Reactor type representing the stream of partial responses. |
Prompt | The request object; identical for synchronous and streaming. |
Advisor | RequestResponseAdvisor interface; applied around both synchronous and streaming calls. |
OpenAiChatModel | Example provider adapter; implements StreamingChatModel for OpenAI's streaming API. |
StreamingChatClientResponseSpec | The internal spec that returns Flux<ChatResponse> from the stream() call. |
These components collaborate to deliver a consistent reactive programming model to the developer.
Source Code Structure
Streaming-related source files are spread across the core and provider modules:
org.springframework.ai.chat.client
├── ChatClient.java // defines stream() in response spec
├── DefaultChatClient.java // implements streaming delegation
org.springframework.ai.chat.model
├── StreamingChatModel.java // the reactive extension interface
├── ChatModel.java // base model interface (call())
org.springframework.ai.openai
├── OpenAiChatModel.java // implements StreamingChatModel for OpenAI
├── OpenAiStreamingChatModel.java // (older variant) may still exist
org.springframework.ai.azure.openai
├── AzureOpenAiChatModel.java // streaming support via Azure
org.springframework.ai.ollama
├── OllamaChatModel.java // streaming support for Ollama
The key separation: the core defines the streaming contract (StreamingChatModel), and each provider module implements it. The ChatClient remains unaware of the provider specifics, interacting only with the interface.
Streaming Execution Lifecycle
The following sequence diagram shows a complete streaming request from the application through to the LLM provider and back.
- Request creation – The application calls
chatClient.prompt().user(...).stream().chatResponse(). TheChatClientbuilds the prompt and prepares the advisor chain. - Advisor pre-processing – Advisors modify the prompt just as in a synchronous call.
- Streaming invocation – The
ChatClientcallsstreamingChatModel.stream(prompt). The provider adapter constructs a streaming HTTP request (e.g., with"stream": true). - Token emission – The provider’s API sends server‑sent events or chunked responses. The adapter parses each chunk into a
ChatResponsecontaining a content delta and metadata (e.g., tool calls, finish reason). - Reactive propagation – The
Flux<ChatResponse>flows back through the framework. Advisors that need to process the response can apply per‑item transformations or attach adoFinallycallback. - Client consumption – The application subscribes to the
Fluxand processes each partial response, often forwarding it to a UI via SSE or WebSockets. - Stream completion – The provider signals the end of the stream, and the
Fluxcompletes.
ChatClient Streaming API
The streaming API is exposed through the ChatClient response spec. The key method is:
ChatClientResponseSpec stream();
Calling stream() returns a StreamingChatClientResponseSpec that offers:
Flux<ChatResponse> chatResponse()– raw model response stream.Flux<String> content()– convenience method that extracts the text content from eachChatResponse.
Internally, DefaultChatClient constructs a DefaultStreamingChatClientResponseSpec which holds a reference to the model and the prompt. When chatResponse() is called, it executes the advisor chain and then invokes chatModel.stream(prompt).
The fluent API is designed so that the request spec is identical for both synchronous and streaming paths. This unified model allows the developer to switch between call() and stream() without changing the prompt construction.
ChatModel Streaming Integration
The StreamingChatModel interface is the backbone of the reactive path. It is defined as:
public interface StreamingChatModel extends ChatModel {
Flux<ChatResponse> stream(Prompt prompt);
}
All major provider adapters implement this interface:
OpenAiChatModel– Opens a streaming connection to the OpenAI/v1/chat/completionsendpoint with"stream": true. It uses a reactiveWebClientto read SSE events, parse them intoChatResponsedeltas, and emit them on aFlux.AzureOpenAiChatModel– Similar to OpenAI but connects to Azure’s streaming endpoint, handling authentication and deployment‑specific URLs.OllamaChatModel– Uses Ollama’s local streaming API; the adapter returns aFluxfrom the response’s line‑delimited JSON stream.DashScopeChatModel(in Spring AI Alibaba) – Implements DashScope’s streaming SSE protocol.
Each adapter encapsulates the transport and parsing logic, so the rest of the framework sees only a uniform Flux<ChatResponse>.
Reactor Flux Integration
Spring AI builds its streaming on Project Reactor, the default reactive library in the Spring ecosystem. Key aspects:
Flux<ChatResponse>– The type returned bystream(). It is a cold publisher: nothing happens until the application subscribes.- Backpressure – The framework respects reactive backpressure. If the consumer is slow, the adapter buffers minimally (typically one chunk) and waits for demand before reading more from the network.
- Cancellation – When the application unsubscribes or cancels the
Flux, the adapter closes the underlying HTTP connection, saving tokens and resources. - Error handling – If the streaming connection breaks or the provider returns an error, the
Fluxsignals anonErrorwith a standard Spring AI exception (AiClientException). - Completion signals – When the stream ends, the
Fluxcompletes withonComplete(). Advisors and theChatClientcan hook into this signal for cleanup.
Reactors Flux provides a rich set of operators that applications can use to transform, filter, or merge streams without blocking threads.
Streaming Response Assembly
In streaming mode, each emitted ChatResponse represents a partial update, not a full answer. The assembly logic differs from the synchronous path:
- Content delta – The
ChatResponsecontains aGenerationwhose output is a delta, not the final accumulated text. The framework does not automatically concatenate deltas; that is the responsibility of the consumer (or a higher-level utility). - Metadata updates – Later chunks may include updated finish reasons or tool call details. The final chunk typically has
finishReason = STOPand the aggregated token usage. - Tool calls – For streaming with tool calling, the model may emit tool call deltas across multiple chunks. The provider adapter assembles these deltas into a complete
ToolCallbefore emitting a consolidatedChatResponse. Spring AI handles this internally for each provider.
The ChatClient provides a convenience method content() that aggregates the text, offering a simpler API for common use cases.
Advisor Integration
Advisors are fully compatible with streaming, but their behaviour must be re‑entrant and non‑blocking.
- Request advisement –
adviseRequest()is called synchronously before the stream starts, exactly as in the non‑streaming case. - Response advisement –
adviseResponse(ChatResponse, Map)is called per emitted item if the advisor is applied as aStreamingResponseAdvisoror if the framework wraps it. Otherwise, it may be called only on the final aggregated response (in thedoFinallyblock). The exact behaviour depends on how the advisor is configured. - Streaming‑aware advisors – An advisor can implement
StreamingAdvisor(sub‑interface) to explicitly handle per‑item callbacks, or it can use adoOnEachpattern via theFlux.
For example, a logging advisor might log each token as it arrives. A content‑filter advisor might drop tokens that contain sensitive data. The framework’s advisor chain is flexible enough to accommodate both use cases.
Tool Calling and Streaming
Tool calling during streaming introduces additional complexity because tool call requests are often assembled incrementally. Key points:
- Delta assembly – The provider adapter buffers tool call deltas across chunks, assembling them into complete
ToolCallobjects before emitting aChatResponsethat contains the tool call. - Execution pause – When a tool call is detected, the stream typically pauses (the model stops emitting tokens), and the tool is executed. The tool result is then injected back into the conversation, and a new stream may be started for the subsequent model call.
- Compatibility – Not all providers support tool calling in streaming mode identically. Spring AI’s adapters handle these differences, presenting a uniform experience.
- Current limitations – Some providers may require special handling (e.g.,
parallel_tool_callsenabled). The framework adapters are continuously updated to keep pace with provider APIs.
The ChatClient and agent runtime manage tool‑calling loops transparently; streaming consumers receive the final answer chunks after all tool calls have resolved.
Design Patterns Used
- Strategy Pattern –
StreamingChatModelis the strategy interface; each provider adapter is a concrete strategy. - Adapter Pattern – Provider adapters translate the uniform
stream()call into provider‑specific protocols and back. - Publisher‑Subscriber (Reactive Streams) – The
Flux<ChatResponse>implements the publisher‑subscriber pattern, decoupling data production from consumption. - Builder Pattern – The
ChatClientuses a builder for configuration and a fluent request/response spec for execution. - Decorator Pattern – The advisor chain can be seen as a decorator around the
ChatModel, adding behavior to the streaming call. - Dependency Injection – The
ChatModeland advisors are injected, allowing easy composition and testing.
Extension Points
Developers can extend streaming in several ways:
- Custom
StreamingChatModel– Implement the interface for a new provider. The adapter should return aFluxthat emitsChatResponseobjects. - Custom advisor – Create an advisor that operates on each stream element (by wrapping the
Fluxwith operators) or that acts as aStreamingAdvisor. - Response transformation – Use Reactor operators (
map,filter,buffer) in the application layer or inside a custom advisor to modify the stream. - Streaming aggregation – Provide a custom
StreamingResponseExtractor(if the framework exposes such an SPI) to control how deltas are combined.
All extensions leverage standard Spring and Reactor mechanisms.
Enterprise Best Practices
- Streaming APIs – Expose streaming endpoints using Spring WebFlux and
application/x-ndjsonor SSE. Forward theFluxdirectly to the HTTP response. - Server‑Sent Events (SSE) – Use
spring-webfluxto return aFlux<ServerSentEvent<String>>that maps each token to an event. - WebFlux integration – Leverage reactive controllers (
@RestControllerreturningFlux) to avoid blocking threads. - Scalability – Reactive streams use fewer threads; the framework can handle thousands of concurrent streaming connections with a small thread pool.
- Timeout handling – Set network timeouts on the provider adapter to avoid hung connections. Reactor’s
timeout()operator can be applied to theFlux. - Error recovery – Use
onErrorResume()orretry()operators in the application layer to gracefully handle transient failures. - Observability – Each streaming chunk can be traced individually; Spring AI’s Micrometer integration records token counts and latency for streaming calls.
Performance Considerations
- Latency – Streaming reduces time‑to‑first‑token, but network round‑trips remain the dominant factor.
- Throughput – Reactive non‑blocking I/O allows a single application instance to manage many concurrent streams without thread exhaustion.
- Memory usage – Streaming avoids buffering the entire response in memory, reducing heap pressure.
- Reactive scheduling – The default Reactor scheduler uses a small number of worker threads; for CPU‑heavy token processing, consider offloading to a dedicated scheduler.
- Backpressure – The framework respects demand; slow consumers will cause the provider adapter to pause reading from the socket, preventing memory overruns.
- High‑concurrency scenarios – Use connection pooling and HTTP client reuse; each provider adapter typically uses a shared
WebClientorRestClientinstance.
Source Code Reading Guide
To understand the streaming internals, follow this order:
StreamingChatModel.java– The core reactive interface.DefaultChatClient.java– Look for thestream()method and the inner classDefaultStreamingChatClientResponseSpec.OpenAiChatModel.java– Trace thestream()implementation; see how it constructs the request and parses SSE events.OllamaChatModel.java– A simpler streaming implementation; good for understanding the pattern.AzureOpenAiChatModel.java– Similar to OpenAI, with Azure-specific authentication.ChatClientResponseSpecandStreamingChatClientResponseSpec– Interfaces that define the streaming return types.- Test classes –
OpenAiChatModelTestsandChatClientStreamingTestsshow end‑to‑end streaming scenarios.
Related Source Code Guides
- ChatClient Source Code Analysis – The facade that initiates streaming.
- Prompt Source Code Analysis – The message structure used in streaming requests.
- ChatModel Source Code Analysis – The base model interface and its streaming variant.
- ChatResponse Source Code Analysis – How partial responses are normalized.
- Advisor Source Code Analysis – The interceptor chain that can wrap streaming calls.
- Tool Calling Source Code Analysis – How tool calls work during streaming.
- Structured Output Source Code Analysis – Converting streamed text to typed objects.
- Memory Source Code Analysis – Works seamlessly with streaming; history is injected before the stream.
- MCP Source Code Analysis – MCP tools can be used in streaming agent loops.
Summary
Spring AI’s streaming support is a first‑class citizen built on Project Reactor and the StreamingChatModel interface. The framework provides a unified programming model across all providers, encapsulating the complexity of SSE, chunk parsing, and backpressure. Understanding the source code reveals how the ChatClient, provider adapters, and the reactive Flux<ChatResponse> pipeline collaborate to deliver low‑latency, resource‑efficient token streaming. This knowledge is essential for building high‑performance, real‑time AI applications and for extending the framework with custom providers or streaming behaviors.
Proceed to ChatModel Source Code Analysis for a deeper understanding of the model abstraction that underpins both synchronous and streaming execution.