Streaming protocol

The UI message stream on the wire, and the server-side builders.

Chat UIs on the web stream messages as SSE frames of typed JSON chunks, terminated by data: [DONE]. swift-ai-sdk speaks that exact protocol on both ends: sessions consume it, and the server helpers on this page produce it. If you already have a chat route, the app plugs into it; if you're writing a Swift server, your web frontend plugs into you.

Serving a stream

UIMessageStream.chunks bridges the generation loop onto protocol chunks, the toUIMessageStreamResponse analog for Swift servers (Vapor, Hummingbird, or anything that writes SSE):

ChatRoute.swift
let result = streamText(model: model, messages: messages, tools: tools)
let chunks = UIMessageStream.chunks(from: result.fullStream)

response.headers = UIMessageStream.headers
for try await chunk in chunks {
  try await response.write(UIMessageStream.encodeSSE(chunk))
}
try await response.write(UIMessageStream.doneSSE)

Building streams by hand

UIMessageStream.build is the createUIMessageStream analog: write arbitrary chunks and merge whole generation streams into one response.

let stream = UIMessageStream.build { writer in
  writer.write(.data(name: "data-status", data: .string("searching")))

  let result = streamText(model: model, prompt: prompt)
  writer.merge(UIMessageStream.chunks(from: result.fullStream))
}

The stream stays open until the body returns and every merged stream drains; errors surface as in-band error chunks.

Message metadata

Attach values at start and stream updates as the loop progresses; the client deep-merges every update into UIMessage.metadata:

UIMessageStream.chunks(
  from: result.fullStream,
  metadata: ["model": .string("claude-sonnet-5")],
  messageMetadata: { part in
    if case .finish(_, let usage) = part {
      return ["totalTokens": .number(Double(usage.totalTokens))]
    }
    return nil
  }
)

Reading streams

readUIMessageStream consumes any chunk stream as a sequence of UIMessage snapshots, one per applied chunk, without a ChatSession. Useful for persistence pipelines and server-side processing:

for try await snapshot in readUIMessageStream(chunks) {
  render(snapshot)
}

Converting to model messages

convertToModelMessages turns client UIMessage state into the model history, including file parts (data URLs decode to inline bytes), settled tool calls with their results, and approval responses for the loop to resolve.

Chunk reference

The chunk types on the wire, for anyone implementing a server or debugging frames:

ChunkPurpose
start, finish, abortMessage lifecycle; start carries the id and metadata
start-step, finish-stepLoop step boundaries
text-start, text-delta, text-endStreamed text, framed by part id
reasoning-start/-delta/-endStreamed thinking
tool-input-start, tool-input-delta, tool-input-availableA tool call assembling
tool-output-available, tool-output-error, tool-output-deniedIts result
tool-approval-requestHuman-in-the-loop pause
source-url, source-documentCitations
data-*Your custom data parts
message-metadataDeep-merged metadata updates
errorIn-band errors

A Swift server and a TypeScript client, or the reverse, agree on every one of these byte for byte.