Class AiServiceStreamingEventPublisher
- All Implemented Interfaces:
Flow.Publisher<AiServiceStreamingEvent>
Flow.Publisher of AiServiceStreamingEvents.
It consumes the model's reactive StreamingChatModel.chat(ChatRequest)
publisher round by round, mapping each low-level ChatModelStreamingEvent to the corresponding high-level
AiServiceStreamingEvent as it arrives (AiServiceStreamingEvent.PartialThinkingEvent, AiServiceStreamingEvent.PartialResponseEvent,
AiServiceStreamingEvent.PartialToolCallEvent, AiServiceStreamingEvent.CompleteToolCallEvent). When a round requests tools, each tool is started
eagerly as soon as its AiServiceStreamingEvent.CompleteToolCallEvent is emitted - executed without blocking via
ToolExecutor.executeAsync(ToolExecutionRequest, InvocationContext)
(emitting AiServiceStreamingEvent.BeforeToolExecutionEvent and AiServiceStreamingEvent.AfterToolExecutionEvent as they happen) - and the round's
ChatResponse is emitted as an AiServiceStreamingEvent.IntermediateResponseEvent once the round's stream completes, after
which the next round's publisher is subscribed to. Exactly one AiServiceStreamingEvent.FinalResponseEvent carrying the final
answer is emitted last, followed by onComplete. If RAG content was retrieved, a single
AiServiceStreamingEvent.RetrievedContentsEvent is emitted first.
Output guardrails. When the AI Service method has output guardrails configured, the final answer may be
rewritten by a guardrail, so the streamed partial chunks are not authoritative. In that case no
AiServiceStreamingEvent.PartialResponseEvent is emitted for the final answer; only the (possibly rewritten)
AiServiceStreamingEvent.FinalResponseEvent carries it. Consumers must therefore rely on AiServiceStreamingEvent.FinalResponseEvent for the
answer whenever output guardrails may be present, rather than concatenating AiServiceStreamingEvent.PartialResponseEvents.
Event ordering. Because tools start eagerly on their AiServiceStreamingEvent.CompleteToolCallEvent (a latency
optimization: a tool overlaps the streaming of the round's remaining tokens and of any later tool calls), a
round's AiServiceStreamingEvent.BeforeToolExecutionEvent / AiServiceStreamingEvent.AfterToolExecutionEvent may be emitted before that
round's AiServiceStreamingEvent.IntermediateResponseEvent. Treat AiServiceStreamingEvent.IntermediateResponseEvent as a per-round marker
carrying the assembled model ChatResponse, not as a barrier that all of the round's
tool-execution events follow. This is the one ordering difference from the handler-based
AiServiceTokenStream/TokenStream, whose intermediate-response callback always precedes that
round's tool callbacks. Otherwise the semantics mirror that handler-based stream: events from every round are
surfaced in order, and no thread is ever blocked or pinned while a model response or a tool result is in flight.
Back-pressure. The model's streaming response is consumed with unbounded demand and its events are
relayed to the subscriber through a bounded buffer (16384 entries by default,
overridable per AI Service via AiServices.streamingBufferSize(int)). The model stream could be
throttled — its subscription demand maps to TCP / HTTP-2 flow control (see onSubscribe) — but we
deliberately request unbounded: throttling the socket cannot slow token generation (already produced and
billed server-side) and only risks an idle-timeout reset of the in-flight response, so the sole thing it would
protect is heap — which the bounded buffer already guards. Consequently a subscriber that consumes slower than
the model produces and overflows the buffer terminates with an IllegalStateException rather than
dropping events (which would corrupt the assembled response) or buffering unbounded (which risks
OutOfMemoryError).
Consumers must therefore not block in onNext; offload heavy per-event work. If a slow-but-correct
consumer trips the buffer on a long response, raise it via AiServices.streamingBufferSize(int)
(or set it to Integer.MAX_VALUE for an effectively unbounded buffer, accepting the OOM risk).
Cancellation. Cancelling the Flow.Subscription stops the interaction: the in-flight model
call is cancelled (for providers whose reactive stream supports it, this aborts the underlying HTTP
request), no further round is started, and no more events — including the terminal
AiServiceStreamingEvent.FinalResponseEvent and onComplete/onError — are emitted. A
tool execution that has already started is not interrupted: it runs to completion and its
result is discarded (Java cannot safely interrupt arbitrary tool code; this is a deliberate best-effort
contract, consistent with the CompletableFuture path).
- Since:
- 1.20.0
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final intDefault size of the bounded back-pressure buffer (see the class-level "Back-pressure" note), used when an AI Service does not override it viaAiServices.streamingBufferSize(int). -
Constructor Summary
ConstructorsConstructorDescriptionAiServiceStreamingEventPublisher(AiServiceTokenStreamParameters parameters, int bufferSize) -
Method Summary
Modifier and TypeMethodDescriptionvoidsubscribe(Flow.Subscriber<? super AiServiceStreamingEvent> subscriber) static Flow.Publisher<String> toTextPublisher(Flow.Publisher<AiServiceStreamingEvent> events, boolean hasOutputGuardrails, int bufferSize) Maps a richAiServiceStreamingEventstream to a text-onlyStringstream.
-
Field Details
-
DEFAULT_BUFFER_SIZE
public static final int DEFAULT_BUFFER_SIZEDefault size of the bounded back-pressure buffer (see the class-level "Back-pressure" note), used when an AI Service does not override it viaAiServices.streamingBufferSize(int).- See Also:
-
-
Constructor Details
-
AiServiceStreamingEventPublisher
-
-
Method Details
-
subscribe
- Specified by:
subscribein interfaceFlow.Publisher<AiServiceStreamingEvent>
-
toTextPublisher
public static Flow.Publisher<String> toTextPublisher(Flow.Publisher<AiServiceStreamingEvent> events, boolean hasOutputGuardrails, int bufferSize) Maps a richAiServiceStreamingEventstream to a text-onlyStringstream. Used to satisfy AI Service methods that return aPublisher<String>(or aFlux<String>/Multi<String>via aPublisherAdapter). The concatenation of all emitted strings equals the final answer.When there are no output guardrails, the text of each
AiServiceStreamingEvent.PartialResponseEventis emitted as it streams (true token-by-token streaming) and every other event is dropped.When there are output guardrails, an output guardrail may rewrite the final answer, so the individual partial chunks are no longer authoritative. In that case only the (possibly rewritten) text of the single
AiServiceStreamingEvent.FinalResponseEventis emitted. This loses no streaming granularity in practice: with output guardrails the rich event stream does not emitAiServiceStreamingEvent.PartialResponseEvents at all (they cannot be reconciled with a possibly-rewritten final answer), so the whole answer would arrive at the very end regardless.The event stream is drained with unbounded demand into a bounded buffer rather than mapped with demand-passing operators (such as mutiny-zero's
Select/Transform). The last event of a round-trip is always aAiServiceStreamingEvent.FinalResponseEvent, which contributes no string when there are no output guardrails: a demand-passing filter would only replenish upstream demand upon receiving that event, so once the subscriber has taken the last string and stops requesting, the trailing event - and theonCompletebehind it - would never be delivered and the stream would hang.- Parameters:
events- the rich event stream to adapthasOutputGuardrails- whether the AI Service method has output guardrails configuredbufferSize- the back-pressure buffer size
-