Docs

Guide

Swift SDK

A Swift package for iOS and macOS apps: a typed async client for the API, and chat that answers on the device first and on Silicon Network when the device cannot.

What it is

SiliconNetwork is written for Swift 6 with strict concurrency, every public type is Sendable, and it has no third-party dependencies (Foundation, CryptoKit and, where the SDK has it, FoundationModels). The network client runs on iOS 15, macOS 12, tvOS 15, watchOS 8 and visionOS 1. Answers on the device need iOS, macOS or visionOS 26 with Apple Intelligence switched on; everywhere else the package still builds and every request goes to the network.

The package is in the repository at sdks/swift and is not published as its own repository yet. Swift Package Manager only resolves packages from the root of a repository, so add it as a local package for now: in Xcode, File > Add Package Dependencies > Add Local, and pick sdks/swift in a clone.

Package.swift
dependencies: [
    .package(path: "../siliconnetwork/sdks/swift"),
],
targets: [
    .target(name: "YourApp", dependencies: [
        .product(name: "SiliconNetwork", package: "SiliconNetwork"),
    ]),
]

Quickstart

Chat.swift
import SiliconNetwork

let client = SiliconNetwork(apiKey: "sn_live_...")

let out = try await client.chat.completions.create(
    ChatCompletionRequest(model: "llama-3.2-3b", messages: [.system("Be brief."), .user("What is MLX?")])
)
print(out.text ?? "")
print(out.meta.model ?? "", out.meta.requestID ?? "")

Anything compiled into an app can be extracted from it. Call the API from your own backend, or give the app a scoped key with a spend cap (see Key protection).

Options: baseURL (the origin without /v1), timeout (seconds, 120), retry (a RetryPolicy, 2 retries), defaultHeaders and your own URLSession. Every call takes options: RequestOptions(headers:timeout:), for headers such as Silicon-Project or X-Silicon-No-Retention: true.

Streaming

Stream.swift
let stream = try await client.chat.completions.stream(
    ChatCompletionRequest(model: "llama-3.2-3b", messages: [.user("Write a haiku about silicon")])
)
for try await chunk in stream {
    print(chunk.text, terminator: "")
}

stream(...) returns once the gateway accepted the call, so a 402 or 429 throws before any output. Iterate it for chunks, or call text() or final() for the joined text or the whole completion with its tool calls and usage. If the serving node fails after output began, the iteration throws SiliconError.stream, so a cut-off answer never looks complete.

Tools, JSON mode and images

Tools.swift
let weather = FunctionTool.function(
    "get_weather",
    parameters: ["type": "object", "properties": ["city": ["type": "string"]], "required": ["city"]]
)
let out = try await client.chat.completions.create(
    ChatCompletionRequest(model: "llama-3.1-8b", messages: [.user("Weather in Oslo?")], tools: [weather])
)
if let call = out.toolCalls.first {
    let args = try JSONValue.parse(call.function.arguments)   // ["city": "Oslo"]
}

// JSON mode, or .jsonSchema(name:schema:strict:)
ChatCompletionRequest(model: "llama-3.1-8b", messages: [.user("Three colors as JSON")], responseFormat: .jsonObject)

// An image in: bytes as a data: URL, or an https URL
ChatCompletionRequest(model: "qwen3-vl-4b",
                      messages: [.user([.text("What is this?"), .image(photo, mimeType: "image/jpeg")])])

embeddings.create and models.list are typed as well, and request(method, path, json:) reaches any other JSON route (Responses, Claude Messages, rerank, Ollama).

Headers, errors and retries

Every result carries meta: requestID (Silicon-Request-Id), model (Silicon-Model), node, cachedTokens, ttftMs, the rate-limit budget, and the receipt when you asked for one. Failures throw SiliconError:

SiliconError
CaseWhen
.unauthorized401: no key, or a revoked or unknown one
.insufficientCredits402: insufficient credits or a spend cap reached
.forbidden403: the key may not use this model, route or IP
.rateLimited(_, retryAfter:)429, with Retry-After in seconds
.server(_, retryAfter:)5xx
.badRequest400, 404, 409, 413, 422
.streaman in-band error after the stream began
.transport(URLError)no HTTP answer: offline, DNS, TLS, timeout

Retries use exponential backoff with jitter and honor Retry-After (capped at 30 seconds): 429, 502, 503 and 504 on every call, because the gateway refuses those before doing or billing any work; 500 and network failures only on GET, and on any call when the connection was never made. A stream is retried only before its first byte.

Receipts

With Silicon-Receipt: 1, meta.receipt holds the receipt and meta.requestSHA256 and meta.responseSHA256 the hashes of the exact bytes. verifyReceipt(receipt, keys:, requestSHA256:, responseSHA256:) checks it with CryptoKit's Ed25519, the same checks as the Python and TypeScript SDKs; client.receiptKeys() fetches the keys. On a stream, read stream.trailer after iterating it. See Verifiable receipts.

On-device first

HybridChat answers on the device with Apple's Foundation Models when it can and on Silicon Network when it cannot. The answer is a chat completion either way, and says where it ran.

Hybrid.swift
let chat = HybridChat(client: SiliconNetwork(apiKey: key))   // networkModel: "llama-3.1-8b"

let out = try await chat.complete(HybridRequest(prompt: "Summarize this note: ...", system: "One sentence."))
print(out.text ?? "")
print(out.ranOn)    // .onDevice or .network
print(out.reason)   // nil on the device, otherwise why not

let stream = try await chat.stream(HybridRequest(prompt: "Tell me a short story"))
for try await chunk in stream { print(chunk.text, terminator: "") }

The routing policy checks, in order, and the first that applies sends the request to the network:

When a request goes to the network
CheckReason
The preference is .networkOnly.networkOnlyPreference
No usable on-device model: an older OS, Apple Intelligence off, the model still downloading.onDeviceUnavailable
A message carries an image.imagesUnsupported
A tool the device does not run.toolsUnsupported
JSON mode or a JSON Schema.structuredOutputUnsupported
A specific model is named that the device does not stand in for.modelNotOnDevice
Tool results in the history, or a last turn that is not the user's.unsupportedConversation
The prompt plus room for the answer does not fit the context window.promptTooLong

The prompt is measured with the model's own tokenizer on OS 26.4 and later, and estimated before that. If the device fails anyway (the context overflowed, its assets went away, it is busy), the network answers with reason .onDeviceFailed, as long as no streamed output went out yet. A guardrail refusal is an answer and is not sent on, unless you set fallbackOnRefusal.

Preferences

.preferOnDevice is the default. .onDeviceOnly never uses the network and throws HybridError.onDeviceRequired(reason) for what the device cannot take; .networkOnly never uses the device. With HybridChat(client: nil) there is no key at all, and requests that need the network throw HybridError.networkUnavailable(reason). chat.plan(request) returns the decision without running anything.

Policy.swift
let policy = RoutingPolicy(
    preference: .preferOnDevice,          // or .onDeviceOnly, .networkOnly
    onDeviceStandIns: ["llama-3.2-1b"],   // models the device may answer for
    reservedOutputTokens: 512,            // room kept for the answer when maxTokens is unset
    fallbackOnFailure: true,
    fallbackOnRefusal: false
)
let chat = HybridChat(client: client, networkModel: "llama-3.1-8b", policy: policy)

Tools on the device

Foundation Models runs tools itself, inside the answer. Pass your Foundation Models tools to SystemLanguageModelBackend(tools:) and give that to HybridChat(onDevice:): requests whose tools all have those names stay on the device and come back as final text. Any other tool goes to the network, where tool calls come back to you to run.

Silicon Network›Docs›Guides›Swift SDK46 items