Stream text
Use streamText when the UI or caller should receive partial output as it is generated.
import SwiftAISDK
@mainstruct StreamTextExample { static func main() async throws { let provider = try AIProviders.openAI() let model = try provider.languageModel("gpt-4.1-mini")
for try await part in model.streamText("Write a short haiku about APIs.") { switch part { case .textDelta(let text): print(text, terminator: "") case .finish(let reason, let usage): print("\nFinished: \(reason ?? "unknown")") print("Tokens: \(usage?.totalTokens ?? 0)") default: break } } }}Streaming emits structured lifecycle parts, not just raw strings. That lets you render text, reasoning, tool input deltas, tool calls, tool approval requests and responses, tool results, source citations, files, raw chunks, response metadata, and finish metadata with one loop.
Built-in HTTP language providers parse the response incrementally, so the first complete provider event can become a LanguageStreamPart while the HTTP response is still open. A provider end marker such as [DONE], caller abort, or stopping iteration cancels the remaining response-body read instead of waiting for EOF.
Canonical Stream Contract
Section titled “Canonical Stream Contract”Built-in language models emit one semantic family for generated content:
textStart, zero or moretextDeltaPartvalues, thentextEnd;reasoningStart, zero or morereasoningDeltaPartvalues, thenreasoningEnd.
The legacy textDelta, reasoningDelta, and finish enum cases remain available for source compatibility with custom models. Built-in providers never pair a legacy delta with a part-aware delta for the same provider token. The high-level facade normalizes legacy deltas into the canonical lifecycle and legacy finish into finishMetadata; a custom stream that mixes both delta families in one semantic channel is rejected because equal adjacent strings can be legitimate provider output and cannot be safely deduplicated by value.
Each logical built-in model response emits exactly one terminal finishMetadata part after its open content and tool-input blocks have been closed and its final usage and provider metadata are known. A protocol that can carry multiple logical responses on one connection, such as OpenAI Responses, emits one terminal part for each response. A multi-step tool loop likewise has one terminal part per model step. Custom models should emit the same terminal; clean custom EOF without one is preserved rather than assigned a guessed finish reason.
The full stream forwards repeatable provider error parts as in-band events. Setup, HTTP, framing, and network failures terminate by throwing. If an in-band error closes a provider stream without a terminal event, the facade completes that step with a single finishMetadata whose reason is error. By default an attempt is not replayed once it has exposed a public part. Callers can opt into the narrower post-start recovery described below.
Use toTextStream() when only text is needed. It emits canonical text deltas, ignores lifecycle, reasoning, tool, metadata, and in-band error parts, and still propagates thrown failures.
Custom Transports
Section titled “Custom Transports”Provider settings continue to accept any AITransport, which keeps unary generate calls source-compatible with send-only transports. A custom transport used by stream must also conform to AIStreamingTransport and return response headers plus an incremental AsyncThrowingStream<Data, Error> body. SwiftAISDK reports a non-retryable AIError.invalidArgument when streaming is requested with a send-only transport; it never falls back to buffering through send.
Prodia language generation and the default protocol implementation remain intentionally simulated streams because their underlying operation is unary. Provider-backed SSE and Amazon EventStream language models use the incremental transport path.
Tool input can arrive before the final tool call. Use toolInputStart, toolInputDelta, and toolInputEnd parts when you want to show streamed arguments in a UI, and handle toolApprovalRequest / toolApprovalResponse parts when a tool requires human or policy approval.
Retry Behavior
Section titled “Retry Behavior”Facade calls retry transient setup failures according to retryPolicy. The default
post-start behavior is still conservative: after the first public part, a stream
failure ends that attempt.
Set streamRetries to a positive count to recover from retryable in-band provider
error events after streaming has started. The failed attempt’s tool input, tool
calls/results, finish, usage, and provider metadata do not become step state for
the replacement attempt. When recovery succeeds, the provider error event and
failed terminal state are suppressed. Text and reasoning already delivered to
the caller cannot be retracted, so a replacement attempt can repeat a prefix;
consumers that enable this option should reconcile visible text accordingly.
Thrown transport/framing failures after public output, non-retryable provider
errors, clean empty streams, and output-schema failures do not consume this
post-start retry budget. retryPolicy continues to govern setup attempts and
backoff independently.
for try await part in AI.streamText( model: model, prompt: "Stream this carefully.", retryPolicy: .none, streamRetries: 1) { // Render parts.}Semantic Stream Timeouts
Section titled “Semantic Stream Timeouts”Use AIStreamTimeoutConfiguration when a streaming caller needs to distinguish
the complete-operation deadline, each model-call step, a stalled first output,
or a stall between output parts:
for try await part in AI.streamText( model: model, prompt: "Stream this carefully.", timeout: AIStreamTimeoutConfiguration( totalNanoseconds: 60_000_000_000, stepNanoseconds: 30_000_000_000, firstChunkNanoseconds: 10_000_000_000, chunkNanoseconds: 15_000_000_000 )) { // Render parts.}The step/first/inter-chunk timers restart for every model-call step. Only semantic
output disarms or resets them: non-empty text, reasoning, or tool-input deltas,
files, reasoning files, and complete tool calls. Metadata, raw keep-alives,
lifecycle markers, tool results, and empty deltas do not extend the deadline.
First/inter-chunk failures throw AIStreamTimeoutError with .firstChunk or
.chunk; step and total failures use AIError.timeout. The legacy flat
timeoutNanoseconds remains available as the total deadline.
Total and step budgets include time spent in retry backoff. A step deadline
stays active after the model stream while client-side tools execute; when it
fires, both the provider request and tool context see an aborted signal whose
reason name is TimeoutError. Typed AI.streamText(..., output: Output.*, timeout: ...) overloads use the same timeout lifecycle.