Skip to content

Streaming Text

streamText is the primary way to get streaming responses from a language model. It calls model.doStream() and returns three RxJS Observables — one for raw events, one for text deltas, and one for accumulated text — so you can subscribe to whichever level of detail your UI needs.

import { streamText } from "rxjs-ai";
const result = streamText({
model: myModel,
messages: [{ role: "user", content: [{ type: "text", text: "Hello!" }] }],
});
result.text$.subscribe((text) => console.log(text));
import { streamText, type LanguageModel, type Message } from "rxjs-ai";
const messages: Message[] = [
{ role: "user", content: [{ type: "text", text: "Explain RxJS in one paragraph." }] },
];
const result = streamText({ model, messages });
// Option 1: accumulated text (most common for UI)
result.text$.subscribe((text) => {
outputElement.textContent = text;
});
// Option 2: individual deltas (for token-by-token rendering)
result.delta$.subscribe((delta) => {
outputElement.textContent += delta;
});
// Option 3: raw stream events (for full control)
result.stream$.subscribe((event) => {
switch (event.type) {
case "text-delta":
console.log("Delta:", event.textDelta);
break;
case "finish":
console.log("Done:", event.finishReason, event.usage);
break;
case "error":
console.error("Error:", event.error);
break;
}
});
OptionTypeDefaultDescription
modelLanguageModelRequiredThe language model to use.
messagesreadonly Message[]RequiredThe conversation messages to send.
systemstringundefinedSystem prompt prepended to the conversation.
temperaturenumberundefinedSampling temperature (0–2).
maxTokensnumberundefinedMaximum tokens to generate.
topPnumberundefinedNucleus sampling threshold.
topKnumberundefinedTop-k sampling limit.
stopSequencesreadonly string[]undefinedSequences that stop generation.
signalAbortSignalundefinedSignal for external cancellation.
onFinish(result: TextResult) => voidundefinedCallback invoked when the stream completes.
PropertyTypeDescription
stream$Observable<TextStreamEvent>Raw stream of all events (text-delta, finish, error). Shared across subscribers.
text$Observable<string>Accumulated text. Each emission contains the full text so far.
delta$Observable<string>Individual text chunks. Each emission is just the new piece.

All three Observables share a single upstream subscription via share(), so subscribing to multiple does not duplicate the model call.

const controller = new AbortController();
const result = streamText({
model,
messages,
signal: controller.signal,
});
result.text$.subscribe({
next: (text) => console.log(text),
error: (err) => {
if (err.name === "AbortError") {
console.log("Stream cancelled");
}
},
});
// Cancel after 2 seconds
setTimeout(() => controller.abort(), 2000);

When the signal fires, all three Observables error with an AbortError and the underlying model stream is torn down immediately.

The onFinish callback is called once when the model completes successfully. It receives a TextResult with the final text, token usage, and finish reason.

const result = streamText({
model,
messages,
onFinish: (finalResult) => {
console.log("Text:", finalResult.text);
console.log("Tokens:", finalResult.usage.totalTokens);
console.log("Reason:", finalResult.finishReason);
},
});
result.delta$.subscribe(); // must subscribe to start the stream

Because streamText returns standard Observables, you can use any RxJS operator:

import { throttleTime } from "rxjs";
result.text$
.pipe(throttleTime(100, undefined, { leading: true, trailing: true }))
.subscribe((text) => {
outputElement.textContent = text;
});
import { race } from "rxjs";
const fast = streamText({ model: modelA, messages });
const slow = streamText({ model: modelB, messages });
race(fast.text$, slow.text$).subscribe((text) => {
outputElement.textContent = text;
});
import { retry } from "rxjs";
result.stream$.pipe(retry(2)).subscribe({
next: (event) => console.log(event),
error: (err) => console.error("Failed after 3 attempts:", err),
});
type TextStreamEvent = TextDeltaEvent | FinishEvent | ErrorEvent;
interface TextDeltaEvent {
readonly type: "text-delta";
readonly textDelta: string;
}
interface FinishEvent {
readonly type: "finish";
readonly text: string;
readonly usage: TokenUsage;
readonly finishReason: FinishReason;
}
interface ErrorEvent {
readonly type: "error";
readonly error: unknown;
}
streamTextgenerateText
ReturnsStreamTextResult with stream$, text$, delta$Observable<TextResult>
EmissionsMultiple (one per token)Single (complete response)
Best forReal-time UI, chat, live previewsBackground tasks, data extraction, batch processing
Model methoddoStream()doGenerate()