Skip to main content
Streaming lets your UI update as the model generates text, rather than waiting for the full response. Chappie’s streaming layer is transport-transparent: it prefers a WebSocket connection for lower overhead, but automatically falls back to HTTP Server-Sent Events (SSE) if WebSocket fails after retries. Your event-handling code stays identical regardless of which transport is active. The model output, tool calls, usage snapshots, and lifecycle events all arrive as strongly-typed ChappieStreamEvent values over an AsyncThrowingStream.

Basic Streaming

Call client.stream(_:) with a prompt string and iterate the resulting AsyncThrowingStream with for try await:
let client = Chappie.client()
for try await event in client.stream("Write a welcome message.") {
    switch event {
    case .textDelta(let text):
        print(text, terminator: "")
    case .usageLimits(let snapshot):
        // Update usage UI
        break
    case .completed:
        break
    default:
        break
    }
}
You can also pass a ChappieInputMessage directly, or a full ChappieResponseRequest via streamResponse(_:), when you need control over message role, fast mode, or per-request options.

Stream Events

Every value your for try await loop receives is a ChappieStreamEvent. Handle each case you care about and ignore the rest with default:
An incremental text chunk from the model. Append each delta to a String buffer to build the full response. Deltas arrive in order and are never empty.
A mid-stream usage snapshot. The snapshot may include a primary window (usedPercent, windowMinutes, resetsAt) and a secondary window. Use it to drive a live usage indicator without polling the rate-limits endpoint.
The model has decided to call a host tool and the SDK is about to execute it. The event carries the ChappieToolExecutionRequest, which contains the tool name, arguments, and the policy applied (automatic, ask, or deny). You can use this event to show a “thinking…” indicator.
The host tool finished executing. The result contains the tool call details, execution status (completed, failed, denied, cancelled), the output string, and timing. The SDK automatically feeds the output back to the model as a follow-up turn.
A tool whose policy is .ask is waiting for explicit user approval. Present a consent prompt; call the toolApprovalHandler closure you registered on the client with .approved() or .denied() to continue.
The model has finished generating for this turn. The stream will end cleanly after this event. Use it to hide loading spinners or finalize your text buffer.
The raw SSE event type and payload string before any parsing. Useful for debugging unexpected server events or building advanced integrations that need access to fields the SDK does not yet surface.

Cancelling a Stream with ChappieStreamHandle

When you need to cancel a stream from outside the iteration loop — for example, when the user taps a stop button — use streamHandle(_:) instead of stream(_:). It returns a ChappieStreamHandle whose cancel() method immediately stops the underlying transport and finishes the event stream.
let handle = client.streamHandle("Draft a reply.")

Task {
    for try await event in handle.events {
        if case .textDelta(let text) = event {
            // append text to your UI buffer
        }
    }
}

// Later, when the user taps Stop:
handle.cancel()
You can also cancel via Swift structured concurrency by wrapping the iteration in a Task and calling cancel() on it:
let streamTask = Task {
    for try await event in client.stream("Write a welcome message.") {
        // handle events
    }
}

// Cancel from anywhere:
streamTask.cancel()
Both approaches cooperate with Swift’s cooperative cancellation model — the loop exits cleanly without throwing an error on cancellation.

Transport Behaviour

WebSocket with Automatic Fallback

By default, Chappie opens a WebSocket connection for every streaming turn. If the WebSocket fails, the SDK retries before falling back to HTTP/SSE for the remainder of the session. Once HTTP/SSE is active, all subsequent turns in that client instance use HTTP/SSE — WebSocket is not re-attempted mid-session.

Replay Divergence Protection

If a reconnection attempt results in the server sending a different continuation than what was already delivered to your UI, the SDK throws ChappieClientError.streamingTransportReplayDiverged.
If your error handler catches .streamingTransportReplayDiverged, the associated String value contains the text that was already delivered to your UI before the divergence. You can use it to display a partial response or prompt the user to retry.

Forcing HTTP/SSE Only

If your environment blocks WebSocket connections or you want deterministic transport for testing, opt into HTTP/SSE exclusively:
let client = Chappie.client(
    streamingTransportPolicy: .httpSSEOnly
)
You can also build a custom policy:
let policy = ChappieStreamingTransportPolicy(
    prefersWebSocket: true,
    maxWebSocketReconnectionAttempts: 3,
    fallsBackToHTTPSSE: true
)
let client = Chappie.client(streamingTransportPolicy: policy)

Streaming With a Full Request

When you need to customise reasoning effort, service tier, or search tool access on a streaming turn, build a ChappieResponseRequest and call streamResponse(_:):
let request = ChappieResponseRequest(
    messages: [
        .user(
            "Summarise today's news.",
            options: ChappieMessageOptions(
                responseOptions: ChappieResponseOptions(
                    enableSearchTool: true,
                    reasoningEffort: .medium
                )
            )
        )
    ]
)

for try await event in client.streamResponse(request) {
    if case .textDelta(let chunk) = event {
        // update UI
    }
}