Skip to content

Streaming Operations

This doc explains how to author TypeSpec streaming operations and what client emitters will generate for them.

TypeSpec supports three kinds of streaming bodies via the @typespec/streams, @typespec/sse, and @typespec/events packages:

  • JsonlStream<T> — a newline-delimited JSON stream where every line is a JSON-encoded T.
  • SSEStream<TEvents> — a server-sent event stream (text/event-stream) carrying heterogeneous events described by an @events union.
  • HttpStream<T, ContentType> — a generic streaming body; using ContentType = "text/event-stream" with an @events union activates the same SSE metadata as SSEStream.

Use JsonlStream<T> when your service responds with newline-delimited JSON lines, each encoding a value of type T.

import "@typespec/streams";
using TypeSpec.Streams;
model Chunk {
text: string;
}
@post
op generate(
@body request: {
prompt: string;
},
): JsonlStream<Chunk>;
# unsupported

Use SSEStream<TEvents> to define a server-sent event stream. Events are described by a union decorated with @Events.events. Each named variant becomes an SSE event type; unnamed variants produce message events.

import "@typespec/sse";
import "@typespec/events";
using TypeSpec.SSE;
using TypeSpec.Events;
model ResponseCreated {
id: string;
}
model ResponseDelta {
delta: string;
}
@events
union ResponseEvents {
@contentType("application/json")
responseCreated: ResponseCreated,
@contentType("application/json")
responseDelta: ResponseDelta,
@contentType("text/plain")
@terminalEvent
"[DONE]",
}
@post
op stream(
@body request: {
prompt: string;
},
): SSEStream<ResponseEvents>;
# unsupported

Mark a variant with @terminalEvent to indicate that receiving this event signals the end of the stream. Client emitters use this to know when to stop reading and close the connection.

import "@typespec/sse";
import "@typespec/events";
using TypeSpec.SSE;
using TypeSpec.Events;
model DeltaEvent {
delta: string;
}
@events
union ChatEvents {
@contentType("application/json")
delta: DeltaEvent,
@contentType("text/plain")
@terminalEvent
"[DONE]",
}
@post
op chat(
@body request: {
message: string;
},
): SSEStream<ChatEvents>;
# unsupported

When your SSE event wraps a separate payload, use @Events.data on the payload property to distinguish the envelope from its content. Emitters expose both the envelope type and the unwrapped payload type through sseMetadata.

import "@typespec/sse";
import "@typespec/events";
using TypeSpec.SSE;
using TypeSpec.Events;
model ChatMessage {
role: string;
content: string;
}
@events
union ChatEvents {
@contentType("application/json")
message: {
done: false,
@data message: ChatMessage,
},
@contentType("text/plain")
@terminalEvent
done: {
done: true,
},
}
@post
op chat(
@body request: {
message: string;
},
): SSEStream<ChatEvents>;
# unsupported

A variant without a name in the union produces an SSE message event — the event: field is omitted on the wire. Use this when your service emits a single homogeneous event type.

import "@typespec/sse";
import "@typespec/events";
using TypeSpec.SSE;
using TypeSpec.Events;
model Info {
desc: string;
}
@events
union BasicEvents {
@contentType("application/json")
Info,
}
op receive(): SSEStream<BasicEvents>;
# unsupported

SSEStream can also be used as a request body when the client sends an event stream to the service.

import "@typespec/sse";
import "@typespec/events";
using TypeSpec.SSE;
using TypeSpec.Events;
model InputEvent {
data: string;
}
@events
union InputEvents {
@contentType("application/json")
inputEvent: InputEvent,
}
op send(stream: SSEStream<InputEvents>): void;
# unsupported

For content types other than JSONL or text/event-stream, define a model with @streamOf and a @body bytes property.

import "@typespec/streams";
using TypeSpec.Streams;
model AudioChunk {
id: string;
}
@streamOf(AudioChunk)
model AudioStream {
@header contentType: "audio/mpeg";
@body body: bytes;
}
op synthesize(
@body request: {
text: string;
},
): AudioStream;
# unsupported