# Swift SDK

> Swift SDK for inference shell agents and apps.

**URL:** https://inference.sh/docs/api/sdk-swift
**Last updated:** 2026-11-07

---

**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
dependencies: [
    .package(url: "https://github.com/inference-sh/sdk-swift", from: "0.1.0"),
],
targets: [
    .target(name: "MyApp", dependencies: [
        .product(name: "InferenceSDK", package: "sdk-swift"),
    ]),
]
```

In Xcode: **File → Add Package Dependencies…** and paste `https://github.com/inference-sh/sdk-swift`.

```swift
import 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](https://github.com/inference-sh/sdk-swift)

---

## API Key

Get a key from [settings → workspace → api keys](https://app.inference.sh/settings/team/keys) and pass it to the client:

```swift
import Foundation
import InferenceSDK

let client = InferenceClient(apiKey: "inf_your_key")
// Or from the environment:
// 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](/docs/api/authentication)

---

## Client API

`InferenceClient` exposes namespaced APIs. Responses are unwrapped from the `{ "data", "messages" }` envelope and errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) 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](#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](#agent-sdk)) |

Every request and response model (`TaskDTO`, `ChatMessageDTO`, `AgentDTO`, `MeResponse`, …) is generated from the API's Go types. `MeResponse` matches [`GET /me`](/docs/api/rest/teams#get-current-user) (`user`, `team` as `TeamDTO`, optional `org`, and `team_view`). 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](/docs/api/rest/overview) or see the [SDK overview — client structure](/docs/api/sdk/overview#client-structure) parity table.

---

## Running Apps

### Basic Usage

```swift
import InferenceSDK

let client = InferenceClient(apiKey: "inf_your_key")

// Run and wait for completion (default)
let task = try await client.tasks.run(ApiAppRunRequest(
    app: "infsh/flux-schnell",
    input: ["prompt": "a lighthouse at dawn, watercolor"]
))

print(task.output)
```

`run` returns the finished `TaskDTO`. A failed task throws `TaskRunError.failed`, a cancelled one `TaskRunError.cancelled(task)`, carrying the task as it ended: its `output` is what the app returned before stopping, or `.null`.

`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
let task = try await client.tasks.run(ApiAppRunRequest(
    app: "my-app",
    setup: ["model": "schnell"],
    input: ["prompt": "hello"]
))
```

### With Progress Updates

`run` follows the task's stream by default and reports every change:

```swift
let task = try await client.tasks.run(
    ApiAppRunRequest(app: "my-app", input: ["prompt": "hello"]),
    options: TaskRunOptions(
        onUpdate: { task in print("status:", task.status.rawValue) },
        onDelta: { delta, seq in print("delta \(seq):", delta) }
    )
)
```

Pass `stream: false` in `TaskRunOptions` to poll the task status instead (`pollIntervalMs`, default 2000).

→ [Streaming](/docs/api/sdk/streaming) · [Polling](/docs/api/sdk/polling)

### Fire and Forget

```swift
let task = try await client.tasks.run(
    ApiAppRunRequest(app: "my-app", input: ["prompt": "hello"]),
    options: TaskRunOptions(wait: false)
)
print("started", task.id)

// later
let current = try await client.tasks.get(task.id)
```

### Task Management

```swift
try await client.tasks.cancel(taskId)
let logs = try await client.tasks.getLogs(taskId)
let page = try await client.tasks.list(CursorListRequest(limit: 20))
```

→ [Tasks REST API](/docs/api/rest/tasks) · [Observability](/docs/runtime/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
let (task, session) = try await client.live(ApiAppRunRequest(
    app: "xai/grok-voice",
    input: ["voice": "eve"],
    function: "talk"
))

Task {
    for await event in session.events {
        switch event {
        case .state(.live): microphone.start()                 // connecting → waiting → live → ended
        case .state(let state): print(state)
        case .binary(let pcm): player.play(pcm)                // an item of the output's binary live field
        case .patch(let patch): print(patch)                   // ["user_text": "..."]
        case .clear: player.flush()                            // the app cut an answer short
        case .error(let field, let message): print(field ?? "-", message)
        case .text(let text): print(text)                      // text the app sent as text, not a patch
        }
    }
}

session.sendBinary(micFrame)                                   // an item of the input's binary live field
session.sendPatch(["events": ["type": "text", "text": "hi"]])  // a JSON frame
session.close()                                                // the function returns; the task completes
let 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
let function = try await client.apps.getByName("xai/grok-voice").version?.functions?["talk"]
let (form, live) = splitLiveSchema(function?.inputSchema)
let microphone = pcmFormat(binaryLiveField(live)?.media)       // PCMFormat(sampleRate: 24000, channels: 1)
```

Given the schemas, the session routes by field name:

```swift
let (_, session) = try await client.live(request, options: OpenSocketOptions(
    inputSchema: function?.inputSchema,
    outputSchema: function?.outputSchema
))
try session.sendField("audio", .binary(micFrame))              // binary: the input's binary live field
try session.sendField("voice", .json("ara"))                   // JSON: an ordinary field
for await event in session.events {
    for update in session.updates(for: event) { show(update.field, update.value) }
}
```

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](/docs/extend/streaming) — 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
import InferenceSDK

@MainActor
func chat() async {
    let session = AgentChatSession(client: client, agent: "my-team/support-agent@latest")

    session.onChange = { state in
        // Every state transition: messages, chat status, connection, errors.
        if let last = state.messages.last { print(last.role.rawValue, last.text) }
    }
    session.callbacks.onTurnEnd = { chat in print("agent finished") }

    await session.sendMessage("What can you help me with?")
}
```

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

```swift
@MainActor
final class ChatModel: ObservableObject {
    @Published private(set) var state = AgentChatState.initial
    let session: AgentChatSession

    init(client: InferenceClient, agent: String) {
        session = AgentChatSession(client: client, agent: agent)
        session.onChange = { [weak self] in self?.state = $0 }
    }

    func send(_ text: String) { Task { await session.sendMessage(text) } }
    func stop() { session.stopGeneration() }
}
```

### Tools, Approvals, and Interrupts

When a tool call needs the user, its invocation (`message.toolInvocations`) waits in `awaitingApproval` or `awaitingInput`:

```swift
try await session.approveTool(invocation.id)
try await session.rejectTool(invocation.id, reason: "not now")
try await session.alwaysAllowTool(invocation.id, toolName: invocation.function.name)

// Client-side tools and widget forms
try await session.submitToolResult(invocation.id, result: #"{"form_data":{"size":"large"}}"#)

// Run-level interrupts (client.agents.listRunInterrupts(runId))
try 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](/docs/api/agent/client-tools) · [Agents REST API — Interrupt gates](/docs/api/rest/agents#interrupt-gates)

### Resuming a Chat

```swift
session.setChatId(existingChatId)   // loads the chat and its latest messages, then streams
let more = await session.loadOlderMessages()
```

### Streaming Responses

Without a session, `runAgentStream` posts a message and streams snapshots of the assistant reply until it finishes:

```swift
let request = ApiAgentRunRequest(agent: "my-team/support-agent@latest", input: LLMInput(text: "hello"))
for try await message in client.runAgentStream(request) {
    print(message.status.rawValue, message.text)
    if message.status.isTerminal { break }
}
```

Each element is a full `ChatMessageDTO` snapshot, not a delta.

### Chat Management

```swift
let agent = try await client.agents.getByName(namespace: "my-team", name: "support-agent")
let versions = try await client.agents.listVersions(agent.id)

let chats = try await client.chats.list(CursorListRequest(limit: 20))
try await client.chats.stop(chatId)
for try await event in client.chats.stream(chatId) {
    switch event {
    case .message(let message, _): print(message.text)
    case .chat(let chat): print("chat", chat.status.rawValue)
    case .delta(let messageId, let delta): print(messageId, delta)
    case .run: break
    }
}
```

---

## 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
let stt = SpeechToText(client: client, app: "elevenlabs/stt")
let text = try await stt.transcribe(wavData)

let tts = TextToSpeech(client: client, app: "infsh/kokoro-tts")
for try await audio in tts.synthesize(reply) {   // long text is chunked; one Data per chunk
    try player.enqueue(audio)
}
```

---

## Error Handling

HTTP failures throw `InferenceError.http(status:body:)` with the API's [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) 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
do {
    let task = try await client.tasks.run(request)
} catch let error as InferenceError {
    // .http(status:body:) carries the API's problem details
    print(error.localizedDescription)
} catch let error as TaskRunError {
    print("task failed:", error.localizedDescription)
}
```

→ [REST overview — response format](/docs/api/rest/overview#response-format) — problem+json shape

---

## Configuration

```swift
let client = InferenceClient(
    baseURL: URL(string: "https://api.inference.sh")!,  // default
    apiKey: key,
    onMessage: { notices in print(notices) }            // server warnings attached to responses
)
```

`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
let file = try await client.files.upload(imageData, filename: "photo.jpg", contentType: "image/jpeg")

let task = try await client.tasks.run(ApiAppRunRequest(
    app: "my-image-app",
    input: ["image": .string(file.uri)]
))
```

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](/docs/api/sdk/files)

---

## REST API (curl)

For simple integrations, you can use the REST API directly with streaming:

```bash
# Single call with streaming
curl -N -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "my-org/assistant@abc123",
    "input": {"text": "Hello!", "role": "user"},
    "stream": true
  }' \
  https://api.inference.sh/agents/run
```

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

---

## What's Next?

- [**JavaScript SDK**](/docs/api/sdk-javascript) — JS/TS SDK reference
- [**Python SDK**](/docs/api/sdk-python) — Python SDK reference
- [**Template Agents**](/docs/api/agent/template) — Use existing agents
- [**REST API**](/docs/api/rest/overview) — Direct HTTP endpoints
