A voice assistant
Live speech both ways, with a client-side tool.
Covers Examples/Features/22-Realtime.swift. You end up talking to a model that
talks back through the speakers and can call your code mid-conversation.
The full app version lives in Apps/RealtimeDemo.
Mint a client secret on your server
Realtime connections use short-lived secrets so your API key never ships in the app:
let model = XaiRealtimeModel("grok-voice-latest")
let secret = try await model.createClientSecret(options: RealtimeClientSecretOptions(
expiresAfterSeconds: 300,
sessionConfig: RealtimeSessionConfig(
tools: getRealtimeToolDefinitions(tools: [weatherTool])
)
))
// return {token, url, expiresAt} to the appOpenAIRealtimeModel("gpt-realtime") and
GoogleRealtimeModel("gemini-2.5-flash-native-audio-preview-09-2025")
mint the same way.
Create the session
Configure the conversation and handle tool calls in one place:
let session = RealtimeSession(
model: XaiRealtimeModel("grok-voice-latest"),
sessionConfig: RealtimeSessionConfig(
instructions: "You are a helpful assistant. Be concise.",
voice: "alloy",
inputAudioTranscription: .init(),
turnDetection: .init(type: .serverVAD)
),
onToolCall: { call in
guard call.name == "getWeather" else { return nil }
return try await weatherTool.execute(call.arguments)
}
)
session.connect(secret: secret)Returning nil from onToolCall defers the answer; submit it later with
session.addToolOutput(callID:output:).
Here weatherTool is the Open-Meteo-backed tool from
An agent with tools, not a canned response.
Wire the audio
You own capture and playback, which on iOS means AVAudioEngine. Feed the mic in as PCM, play the response out:
// Speak: 16-bit PCM at the session rate (24 kHz default)
session.sendAudio(microphoneChunk)
// Listen: decoded audio chunks for your player
for await chunk in session.audioOutput {
player.play(chunk)
}When the user interrupts and your player stops, tell the model how much was heard so its memory matches reality:
session.playbackInterrupted(playedMilliseconds: player.playedMilliseconds)Render the conversation
session.messages is regular UIMessage state: transcripts stream in
as text parts, your speech shows up transcribed, and tool calls appear
as tool parts. The same rendering code as a chat screen works here.
Typed input works alongside voice: session.sendText("Hello!").
Final code
import AI
@available(iOS 17.0, macOS 14.0, *)
@MainActor
func startVoiceSession(
secret: RealtimeClientSecret,
weatherTool: Tool
) -> RealtimeSession {
let session = RealtimeSession(
model: XaiRealtimeModel("grok-voice-latest"),
sessionConfig: RealtimeSessionConfig(
instructions: "You are a helpful assistant. Be concise.",
inputAudioTranscription: .init(),
turnDetection: .init(type: .serverVAD)
),
onToolCall: { call in
guard call.name == "getWeather" else { return nil }
return try await weatherTool.execute(call.arguments)
}
)
session.connect(secret: secret)
session.sendText("Hello!")
Task {
for await chunk in session.audioOutput {
// hand PCM to your AVAudioEngine player
_ = chunk
}
}
return session
}The Apps/RealtimeDemo app adds the AVAudioEngine player and mic
capture, the settings sheet, and barge-in, in about 200 lines of
SwiftUI.