Guides
- Quickstart
- Authentication and keys
- Key protection
- Models, aliases and fallbacks
- Automatic model choice (silicon/auto)
- Chat completions
- Streaming
- Structured outputs and tools
- Logprobs
- Vision
- Responses, Claude and Ollama
- Embeddings, rerank, audio, images
- Files and batches
- Documents and search
- LoRA adapters
- Project tags and costs
- Prefix caching
- Headers, rate limits and spend caps
- Errors
- Privacy and retention
- Verifiable receipts
- Data residency
- Pricing
- Billing
- Pay per request (x402)
- On-chain oracle
- Agent credit (backed compute lines)
- Status
- SDKs
- Swift SDK
- Integrations
- Run a node
- Architecture
- Changelog
API reference
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.
dependencies: [
.package(path: "../siliconnetwork/sdks/swift"),
],
targets: [
.target(name: "YourApp", dependencies: [
.product(name: "SiliconNetwork", package: "SiliconNetwork"),
]),
]Quickstart
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
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
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:
| Case | When |
|---|---|
| .unauthorized | 401: no key, or a revoked or unknown one |
| .insufficientCredits | 402: insufficient credits or a spend cap reached |
| .forbidden | 403: the key may not use this model, route or IP |
| .rateLimited(_, retryAfter:) | 429, with Retry-After in seconds |
| .server(_, retryAfter:) | 5xx |
| .badRequest | 400, 404, 409, 413, 422 |
| .stream | an 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.
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:
| Check | Reason |
|---|---|
| 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.
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.