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, create, get, list, cancel, delete; getLogs, getTimings, and getTelemetry for observability; visibility |
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
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