Swift SDK for inference shell agents and apps.
The Swift SDK runs apps and chats with agents from iOS, macOS, and server-side Swift. It uses Swift concurrency (async/await, AsyncThrowingStream), and its Codable models are generated from the API's own types, the same source as the JavaScript and Python SDKs.
Installation
Add the package with Swift Package Manager. In Package.swift:
1dependencies: [2 .package(url: "https://github.com/inference-sh/sdk-swift", from: "0.1.0"),3],4targets: [5 .target(name: "MyApp", dependencies: [6 .product(name: "InferenceSDK", package: "sdk-swift"),7 ]),8]In Xcode: File → Add Package Dependencies… and paste https://github.com/inference-sh/sdk-swift.
1import InferenceSDKRequirements
- Swift 5.9+
- iOS 17+ / macOS 14+
- Linux (Foundation + FoundationNetworking; the library uses no Apple-only frameworks)
Source: github.com/inference-sh/sdk-swift
API Key
Get a key from settings → workspace → api keys and pass it to the client:
1import Foundation2import InferenceSDK34let client = InferenceClient(apiKey: "inf_your_key")5// Or from the environment:6// let client = InferenceClient(apiKey: ProcessInfo.processInfo.environment["INFERENCE_API_KEY"] ?? "")Do not ship a raw API key inside an app binary. For a public app, call the API from your own backend (or proxy it) and hand the client a short-lived key.
Client API
InferenceClient exposes namespaced APIs. Responses are unwrapped from the { "data", "messages" } envelope and errors are RFC 9457 problem details.
| Property | Purpose |
|---|---|
client.tasks | run, watch, create, get, list, cancel, delete; getLogs, getTimings, and getTelemetry for observability; visibility |
client.live | Run a stream function and dial its socket (see Live apps and stream sockets) |
client.sockets | open (dial a stream task's socket), get, list, forTask, access, delete |
client.files | upload, list, get, delete |
client.agents | list, get, getByName, create, update, versions, duplicate, visibility, A2A card, run interrupts |
client.chats | list, get, update, delete, getStatus, stop, cancelMessage, stream |
client.apps | list, get, getByName, create, update, versions, visibility, licenses |
client.search | search and suggest |
AgentChatSession | Stateful agent chat (see Agent SDK) |
Every request and response model (TaskDTO, ChatMessageDTO, AgentDTO, …) is generated from the API's Go types. Enums tolerate values the API adds later: an unknown status decodes instead of failing.
client.uploadFile() remains available as the older form of client.files.upload().
For features not listed above, use the REST API or see the SDK overview — client structure parity table.
Running Apps
Basic Usage
1import InferenceSDK23let client = InferenceClient(apiKey: "inf_your_key")45// Run and wait for completion (default)6let task = try await client.tasks.run(ApiAppRunRequest(7 app: "infsh/flux-schnell",8 input: ["prompt": "a lighthouse at dawn, watercolor"]9))1011print(task.output)run returns the finished TaskDTO. A failed task throws TaskRunError.failed, a cancelled one TaskRunError.cancelled.
input and setup are JSONValue, which accepts Swift literals directly: strings, numbers, booleans, arrays, dictionaries, and nil.
Setup Parameters
Setup parameters configure the app instance (for example, model selection). Workers with matching setup are "warm" and skip setup:
1let task = try await client.tasks.run(ApiAppRunRequest(2 app: "my-app",3 setup: ["model": "schnell"],4 input: ["prompt": "hello"]5))With Progress Updates
run follows the task's stream by default and reports every change:
1let task = try await client.tasks.run(2 ApiAppRunRequest(app: "my-app", input: ["prompt": "hello"]),3 options: TaskRunOptions(4 onUpdate: { task in print("status:", task.status.rawValue) },5 onDelta: { delta, seq in print("delta \(seq):", delta) }6 )7)Pass stream: false in TaskRunOptions to poll the task status instead (pollIntervalMs, default 2000).
Fire and Forget
1let task = try await client.tasks.run(2 ApiAppRunRequest(app: "my-app", input: ["prompt": "hello"]),3 options: TaskRunOptions(wait: false)4)5print("started", task.id)67// later8let current = try await client.tasks.get(task.id)Task Management
1try await client.tasks.cancel(taskId)2let logs = try await client.tasks.getLogs(taskId)3let page = try await client.tasks.list(CursorListRequest(limit: 20))→ Tasks REST API · Observability
Live apps and stream sockets
Stream functions keep a duplex socket open for the life of the task: audio and messages go both ways until the caller closes or the app returns. client.live starts the task and dials its socket:
1let (task, session) = try await client.live(ApiAppRunRequest(2 app: "xai/grok-voice",3 input: ["voice": "eve"],4 function: "talk"5))67Task {8 for await event in session.events {9 switch event {10 case .state(.live): microphone.start() // connecting → waiting → live → ended11 case .state(let state): print(state)12 case .binary(let pcm): player.play(pcm) // an item of the output's binary live field13 case .patch(let patch): print(patch) // ["user_text": "..."]14 case .clear: player.flush() // the app cut an answer short15 case .error(let field, let message): print(field ?? "-", message)16 case .text(let text): print(text) // text the app sent as text, not a patch17 }18 }19}2021session.sendBinary(micFrame) // an item of the input's binary live field22session.sendPatch(["events": ["type": "text", "text": "hi"]]) // a JSON frame23session.close() // the function returns; the task completes24let result = try await client.tasks.watch(task.id) // what it returnedsession.events is an AsyncStream<LiveEvent> that ends when the session does. await session.ended gives the LiveEnd (close code, reason, byCaller, taskEnded).
- Send once it is live. The session is
waitinguntil the app's first frame, which includes a cold start. Frames sent before the relay accepts the connection are dropped (sendBinaryreturnsfalse), and the relay holds only 64 while the app is not there. - It ends on its own when the task does. If the task fails or completes before the app connects, the session ends with
taskEndedand the task's error as the reason. - It redials while waiting. When the relay restarts under a session that has not reached the app, the session dials again with a fresh credential, up to five times.
- A task whose caller never connects keeps waiting, up to 15 minutes. When a session ends with code 1006 (the dial failed, the network dropped), dial again with
client.sockets.open(task.id)or callclient.tasks.cancel(task.id). - Keep the session. Releasing it closes the socket.
After the app relaunches, client.sockets.open(taskId) issues a new credential and dials the running task's socket.
Live fields
What a function's socket carries is in its schemas: a live field is {"type": "array", "format": "stream", "items": ...}. splitLiveSchema separates the ordinary fields (the request body) from the live ones, and pcmFormat reads the format of a PCM audio field:
1let function = try await client.apps.getByName("xai/grok-voice").version?.functions?["talk"]2let (form, live) = splitLiveSchema(function?.inputSchema)3let microphone = pcmFormat(binaryLiveField(live)?.media) // PCMFormat(sampleRate: 24000, channels: 1)Given the schemas, the session routes by field name:
1let (_, session) = try await client.live(request, options: OpenSocketOptions(2 inputSchema: function?.inputSchema,3 outputSchema: function?.outputSchema4))5try session.sendField("audio", .binary(micFrame)) // binary: the input's binary live field6try session.sendField("voice", .json("ara")) // JSON: an ordinary field7for await event in session.events {8 for update in session.updates(for: event) { show(update.field, update.value) }9}The SDK moves frames; recording and playing 16-bit PCM is your app's (AVAudioEngine on Apple platforms). The socket is a URLSessionWebSocketTask. On Linux that needs a libcurl built with WebSockets, which the official Swift images do not have: pass OpenSocketOptions(dial:) with a LiveSocket of your own there.
→ Streaming and Live Functions — live fields, the wire format, and writing the app side
Agent SDK
Chat with AI agents programmatically.
Chat Sessions
AgentChatSession is the full agent chat state machine. It creates the chat on the first message, follows the chat's server-sent event stream, applies token deltas to the right message, reconnects, and pages older messages. It is a port of the JavaScript SDK's agent actions and reducer, and has no UI framework dependency, so it can drive SwiftUI, UIKit, AppKit, or a CLI.
1import InferenceSDK23@MainActor4func chat() async {5 let session = AgentChatSession(client: client, agent: "my-team/support-agent@latest")67 session.onChange = { state in8 // Every state transition: messages, chat status, connection, errors.9 if let last = state.messages.last { print(last.role.rawValue, last.text) }10 }11 session.callbacks.onTurnEnd = { chat in print("agent finished") }1213 await session.sendMessage("What can you help me with?")14}AgentChatSession is @MainActor. Its state is an AgentChatState:
| Field | Description |
|---|---|
messages | [ChatMessageDTO], in order. message.text joins the text blocks; content holds reasoning, images, files, and errors |
chat | The ChatDTO, including busy state and queued messages |
connectionStatus | .idle, .connecting, .streaming, or .error |
error | The last error, if any. clearError() resets it |
hasOlderMessages | More history to page in with loadOlderMessages() |
Sending while the agent is still working queues the message on the server; it runs when the current turn ends. cancelMessage(_:) removes a queued message.
SwiftUI
Mirror the state into an ObservableObject (or @Observable) and render from it:
1@MainActor2final class ChatModel: ObservableObject {3 @Published private(set) var state = AgentChatState.initial4 let session: AgentChatSession56 init(client: InferenceClient, agent: String) {7 session = AgentChatSession(client: client, agent: agent)8 session.onChange = { [weak self] in self?.state = $0 }9 }1011 func send(_ text: String) { Task { await session.sendMessage(text) } }12 func stop() { session.stopGeneration() }13}Tools, Approvals, and Interrupts
When a tool call needs the user, its invocation (message.toolInvocations) waits in awaitingApproval or awaitingInput:
1try await session.approveTool(invocation.id)2try await session.rejectTool(invocation.id, reason: "not now")3try await session.alwaysAllowTool(invocation.id, toolName: invocation.function.name)45// Client-side tools and widget forms6try await session.submitToolResult(invocation.id, result: #"{"form_data":{"size":"large"}}"#)78// Run-level interrupts (client.agents.listRunInterrupts(runId))9try await session.resolveInterrupt(interruptId, decision: "allow") // or "deny"Tools that render UI (A2UI widgets) carry it on invocation.widget or in the result. Widget.parse(string:) reads every widget format the web app accepts.
→ Client Tools · Agents REST API — Interrupt gates
Resuming a Chat
1session.setChatId(existingChatId) // loads the chat and its latest messages, then streams2let more = await session.loadOlderMessages()Streaming Responses
Without a session, runAgentStream posts a message and streams snapshots of the assistant reply until it finishes:
1let request = ApiAgentRunRequest(agent: "my-team/support-agent@latest", input: LLMInput(text: "hello"))2for try await message in client.runAgentStream(request) {3 print(message.status.rawValue, message.text)4 if message.status.isTerminal { break }5}Each element is a full ChatMessageDTO snapshot, not a delta.
Chat Management
1let agent = try await client.agents.getByName(namespace: "my-team", name: "support-agent")2let versions = try await client.agents.listVersions(agent.id)34let chats = try await client.chats.list(CursorListRequest(limit: 20))5try await client.chats.stop(chatId)6for try await event in client.chats.stream(chatId) {7 switch event {8 case .message(let message, _): print(message.text)9 case .chat(let chat): print("chat", chat.status.rawValue)10 case .delta(let messageId, let delta): print(messageId, delta)11 case .run: break12 }13}Speech
TextToSpeech and SpeechToText wrap any speech app on inference shell. They read the app's input and output schema, so the same code works across providers:
1let stt = SpeechToText(client: client, app: "elevenlabs/stt")2let text = try await stt.transcribe(wavData)34let tts = TextToSpeech(client: client, app: "infsh/kokoro-tts")5for try await audio in tts.synthesize(reply) { // long text is chunked; one Data per chunk6 try player.enqueue(audio)7}Error Handling
HTTP failures throw InferenceError.http(status:body:) with the API's RFC 9457 problem details in body. localizedDescription uses detail, then title, and falls back to the raw body (for example HTTP 402: Insufficient balance). Failed and cancelled tasks throw TaskRunError.
1do {2 let task = try await client.tasks.run(request)3} catch let error as InferenceError {4 // .http(status:body:) carries the API's problem details5 print(error.localizedDescription)6} catch let error as TaskRunError {7 print("task failed:", error.localizedDescription)8}→ REST overview — response format — problem+json shape
Configuration
1let client = InferenceClient(2 baseURL: URL(string: "https://api.inference.sh")!, // default3 apiKey: key,4 onMessage: { notices in print(notices) } // server warnings attached to responses5)InferenceClient is a Sendable value type. Create one and share it.
File Upload
Uploads go straight to storage through a presigned URL. Pass the returned uri as app input:
1let file = try await client.files.upload(imageData, filename: "photo.jpg", contentType: "image/jpeg")23let task = try await client.tasks.run(ApiAppRunRequest(4 app: "my-image-app",5 input: ["image": .string(file.uri)]6))App outputs that are files come back as URLs. TaskResultDTO.fileURL(_:) reads either shape apps use (a bare URL or {"uri": ...}), and client.download(_:) fetches the bytes.
→ Files
REST API (curl)
For simple integrations, you can use the REST API directly with streaming:
1# Single call with streaming2curl -N -X POST \3 -H "Authorization: Bearer YOUR_API_KEY" \4 -H "Content-Type: application/json" \5 -d '{6 "agent": "my-org/assistant@abc123",7 "input": {"text": "Hello!", "role": "user"},8 "stream": true9 }' \10 https://api.inference.sh/agents/runThe -N flag disables curl's buffering so you see events in real-time.
What's Next?
- JavaScript SDK — JS/TS SDK reference
- Python SDK — Python SDK reference
- Template Agents — Use existing agents
- REST API — Direct HTTP endpoints