Swift / iOS SDK
The official nRouter Swift SDK for iOS, macOS, watchOS, and visionOS with Swift Package Manager support, async/await, and zero third-party dependencies.
Last updated
The official nRouter Swift SDK (NRouter) provides zero-dependency, idiomatic Swift bindings for Apple platforms including iOS 15+, macOS 12+, tvOS 15+, watchOS 8+, and visionOS 1+. Built natively on URLSession and modern Swift async/await structured concurrency, it connects directly to https://api.nrouter.ai/v1 with automated response metadata parsing, Server-Sent Events (SSE) streaming, and real-time cost attribution.
By communicating through nRouter, native Apple applications gain centralized access to major AI models (OpenAI, Anthropic, Google Gemini, and open-source models) through a unified client, with server-side prompt injection defense and organization guardrails applied before tokens reach downstream providers.
Prerequisites & Installation
The SDK requires Swift 5.9+ and Xcode 15+. It introduces zero third-party dependencies, preserving lean app binary sizes.
Swift Package Manager (Package.swift)
Add the package dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/nRouterGateway/nrouter-sdk.git", from: "2.2.1")
]Then add NRouter to your executable or framework target:
.target(
name: "YourApp",
dependencies: [
.product(name: "NRouter", package: "nrouter-sdk")
]
)Xcode GUI Installation
- Open your project in Xcode.
- Navigate to File → Add Package Dependencies...
- In the search field, enter:
https://github.com/nRouterGateway/nrouter-sdk.git - Select the Dependency Rule: Up to Next Major from 2.2.1.
- Check
NRouterand add it to your target application.
Setup & Configuration
Configure authentication based on your deployment environment.
Environment Variable (macOS & Server-Side Swift)
For command-line tools, background daemons, or server-side Swift (Vapor), export your key:
export NROUTER_API_KEY="sk-nrouter-xxxxxxxxxxxx"Initialize the client with automatic environment resolution:
import NRouter
// Automatically reads NROUTER_API_KEY and targets https://api.nrouter.ai/v1
let client = try NRouter()Direct Initialization & iOS Security
On iOS, watchOS, and visionOS, shell environment variables are unavailable. Pass your virtual key directly:
import NRouter
let client = try NRouter(
apiKey: "sk-nrouter-xxxxxxxxxxxx",
baseUrl: "https://api.nrouter.ai/v1"
)Security Invariant: Never bundle permanent administrative API keys inside production iOS or macOS binaries. Mobile binaries can be decompiled and network traffic inspected. Always mint short-lived, budget-capped virtual keys from your backend authentication service and pass them to client devices.
Configuration Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
apiKey | String | Environment | nRouter virtual key (sk-nrouter-...). |
baseUrl | String | https://api.nrouter.ai/v1 | Unified gateway base URL. Must include /v1. |
timeout | TimeInterval | 60.0 | Total request timeout in seconds for URLSessionConfiguration. |
defaultHeaders | [String: String] | [:] | Custom headers attached to every call (e.g. x-client-name). |
import Foundation
import NRouter
// Advanced configuration with custom timeout and a default header
let client = try NRouter(
apiKey: "sk-nrouter-xxxxxxxxxxxx",
baseUrl: "https://api.nrouter.ai/v1",
timeout: 30.0,
defaultHeaders: [
"x-client-name": "checkout-service"
]
)Implementation Patterns
1. Chat Completion
Execute a synchronous completion and unpack the resulting JSON payload:
import NRouter
let client = try NRouter()
let payload: [String: Any] = [
"model": "gpt-5.4-mini",
"messages": [
["role": "system", "content": "You are a concise iOS engineer."],
["role": "user", "content": "Explain Swift structured concurrency."]
],
"max_completion_tokens": 512
]
let result = try await client.chatCompletions(payload)
if let choices = result["choices"] as? [[String: Any]],
let firstChoice = choices.first,
let message = firstChoice["message"] as? [String: Any],
let content = message["content"] as? String {
print("Response:\n\(content)")
}2. Real-time Streaming with AsyncThrowingStream
Stream tokens directly to your SwiftUI views as they arrive:
import SwiftUI
import NRouter
@MainActor
class ChatViewModel: ObservableObject {
@Published var streamedText = ""
@Published var isGenerating = false
private let client = try? NRouter()
func streamCompletion(prompt: String) async {
guard let client = client else { return }
isGenerating = true
streamedText = ""
let payload: [String: Any] = [
"model": "claude-haiku-4-5-20251001",
"messages": [["role": "user", "content": prompt]],
"stream": true
]
do {
let stream = try await client.chatCompletionsStream(payload)
for try await chunk in stream {
if let delta = chunk.deltaContent {
self.streamedText += delta
}
}
} catch {
print("Stream error: \(error.localizedDescription)")
}
isGenerating = false
}
}3. Per-Request Gateway Overrides
Attach template IDs or cache preferences within the request dictionary:
let payload: [String: Any] = [
"model": "gpt-5.5",
"messages": [["role": "user", "content": "Analyze quarterly report"]],
"nrouter_prompt_template_id": "tmpl_financial_brief_v1",
"nrouter_prompt_variables": ["currency": "USD"],
"nrouter_cache": true
]
let response = try await client.chatCompletions(payload)Production Best Practices
Deterministic Routing & Fallbacks
There are two ways to ask for a model. Direct — you choose the model. nRouter never swaps it for another; a different model answers only through a fallback you configured yourself, and only when the provider cannot take the request. nrouter/auto — nRouter chooses an eligible model for you. The model string you send selects the way:
let payload: [String: Any] = [
// nRouter chooses an eligible model; x-nr-model names the one that answered
"model": "nrouter/auto",
"messages": [["role": "user", "content": "Verify receipt image"]]
]- The
nrouter/autopreference is an organization setting. It is chosen on thenrouter/autotab of Router Settings: Best for the task (default) uses smart model selection when it is available; Lowest cost orders the models fit for the task by their list rate. No request header selects either way. Either way you pay the served model's list price. - Fallbacks for one call. On a directly named model, put
nrouter_fallbacks(one to four model names) in the JSON request body. See Per-Request Options. - When another model is tried. Only when the provider cannot take the request: an upstream
429,503or Anthropic529, a401or404from a misconfigured provider deployment, a connection that never opened, or a provider that sends no first byte in time. A500,502,504or a read timeout after the connection opened is returned to the caller. - What the response tells you.
x-nr-routingis a response header:direct, orfallback:<n>when a later entry answered.x-nr-modelnames the model that answered.
Latency Tuning on Apple Platforms
- Pre-warmed URLSession: Initialize
NRouterduring application launch to pre-warm DNS resolution and TLS handshakes before user actions. - Keep-Alive Connections: Default
URLSessionconfigurations maintain persistent HTTP/2 connections to reduce connection latency. - Cancel Unread Streams: In SwiftUI views, tie tasks to view lifecycles (
.task(id: ...)). When the view disappears, Swift task cancellation aborts the underlying network stream immediately, preventing wasted tokens.
Response Telemetry & Spend Tracking
Inspect gateway metadata on completions:
if let meta = result["_meta"] as? [String: Any] {
let requestId = meta["request_id"] as? String ?? "unknown"
let cost = meta["cost"] as? Double ?? 0.0
let model = meta["model"] as? String ?? "unknown"
print("Served by: \(model) | ID: \(requestId) | Cost: $\(cost)")
}Troubleshooting & Error Handling
The Swift SDK surfaces errors through the NRouterError type.
Common Error Codes
| Status | Code | Cause | Recommended Action |
|---|---|---|---|
400 | guardrail_blocked | Input rejected by server-side content or injection guardrails | Verify prompt safety; review guardrail settings in nRouter dashboard. |
401 | invalid_api_key | Missing, incorrect, or expired virtual API key | Re-authenticate client; mint a fresh virtual key from your backend. |
402 | insufficient_credits | Not enough credits, or a budget ceiling was hit | Add funds in dashboard or update key ceiling. |
429 | rate_limit_exceeded | Client exceeded RPM/TPM quota | Implement backoff retry logic. |
500 / 503 | service_unavailable | Downstream provider error or network disruption | Save a fallback list for the model in Router Settings or send nrouter_fallbacks: another model is tried on 503, not on 500. |
Error Catching Pattern
import NRouter
do {
let response = try await client.chatCompletions(payload)
print("Success: \(response)")
} catch let error as NRouterError {
switch error {
case .guardrailBlocked(let reason):
print("Blocked by security policy: \(reason)")
case .authenticationFailed:
print("Invalid virtual key: Please re-authenticate.")
case .insufficientCredits:
print("Account budget ceiling reached.")
case .rateLimited(let retryAfter):
print("Rate limited: Retry after \(retryAfter ?? 1)s.")
case .serverError(let status, let msg):
print("Gateway error [\(status)]: \(msg)")
case .networkError(let underlying):
print("Network transport error: \(underlying.localizedDescription)")
}
} catch {
print("Unexpected error: \(error)")
}Next Steps
- Python SDK Guide — Server-side Python SDK documentation
- Node.js SDK Guide — TypeScript client for web and backend services
- Chat Completions API — HTTP endpoint specifications