Swift SDK

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:

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.

swift
1import InferenceSDK

Requirements

  • 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:

swift
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.

→ Authentication


Client API

InferenceClient exposes namespaced APIs. Responses are unwrapped from the { "data", "messages" } envelope and errors are RFC 9457 problem details.

PropertyPurpose
client.tasksrun, watch, create, get, list, cancel, delete; getLogs, getTimings, and getTelemetry for observability; visibility
client.liveRun a stream function and dial its socket (see Live apps and stream sockets)
client.socketsopen (dial a stream task's socket), get, list, forTask, access, delete
client.filesupload, list, get, delete
client.agentslist, get, getByName, create, update, versions, duplicate, visibility, A2A card, run interrupts
client.chatslist, get, update, delete, getStatus, stop, cancelMessage, stream
client.appslist, get, getByName, create, update, versions, visibility, licenses
client.searchsearch and suggest
AgentChatSessionStateful 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

swift
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:

swift
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:

swift
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).

→ Streaming · Polling

Fire and Forget

swift
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

swift
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:

swift
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 returned

session.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 waiting until the app's first frame, which includes a cold start. Frames sent before the relay accepts the connection are dropped (sendBinary returns false), 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 taskEnded and 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 call client.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:

swift
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:

swift
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.

swift
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:

FieldDescription
messages[ChatMessageDTO], in order. message.text joins the text blocks; content holds reasoning, images, files, and errors
chatThe ChatDTO, including busy state and queued messages
connectionStatus.idle, .connecting, .streaming, or .error
errorThe last error, if any. clearError() resets it
hasOlderMessagesMore 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:

swift
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:

swift
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

swift
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:

swift
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

swift
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:

swift
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.

swift
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

swift
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:

swift
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:

bash
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/run

The -N flag disables curl's buffering so you see events in real-time.


What's Next?

we use cookies

we use cookies to ensure you get the best experience on our website. for more information on how we use cookies, please see our cookie policy.

by clicking "accept", you agree to our use of cookies.
learn more.