Swift SDK
The Swift SDK wraps the models.manashiki.com API: list the
catalog, resolve a signed download URL, stream the artifact to disk, and
verify its SHA-256 — all with async/await. It is distributed
through a read-only SwiftPM mirror at
Nozium/manashiki-models-swift.
For bug reports and support, write to
contact@utakata-scifi.com.
1. Install via SwiftPM
// Package.swift — track main until the first version tag is published
dependencies: [
.package(url: "https://github.com/Nozium/manashiki-models-swift", branch: "main"),
]
From Xcode: File → Add Package Dependencies, paste
https://github.com/Nozium/manashiki-models-swift, and pick
the Branch: main dependency rule (version rules resolve
once the first semver tag ships). Do not open PRs against the mirror —
branch protection blocks them; CI syncs it from the monorepo.
| Requirement | Value |
|---|---|
| iOS | 17+ |
| macOS | 14+ |
| visionOS | 1+ |
| tvOS | 17+ |
| Swift tools | 5.9 |
| Dependency | swift-crypto 3.0.0+ (CryptoKit is used where available) |
2. Pick the right API key
The SDK validates the key format locally in the initializer and throws
APIError.invalidAPIKey before any network call. Keys are
sent on every request as the X-API-Key header. Only one key
kind is safe to embed in a shipped binary:
| Prefix | Tier | Where it belongs |
|---|---|---|
mk_pub_<org>_… | client | App bundles. The only embeddable mk_* kind. |
mk_test_<org>_… | server (test env) | Staging / dev tooling. Keep out of distributed builds. |
mk_live_<org>_… | server | Your production backend only. |
mk_svc_<org>_… | server | Agents and CI. |
pk_live_* / pk_test_* | client (legacy) | Older integrations; still accepted. |
sk_live_* / sk_test_* | server (legacy) | Must never ship in an app binary. |
Server-only methods — createSession(principalId:ttlSec:)
(POST /api/session) and
createModelUpload(_:) (POST /api/uploads/model)
— throw APIError.forbiddenScope when called with a
client-tier key, so a leaked mk_pub_* key cannot mint
sessions or upload artifacts.
3. Initialize the client
import Manashiki // Throws APIError.invalidAPIKey on a malformed key — before any network call. let client = try ManashikiClient(apiKey: "mk_pub_acme_...") // Full initializer: // init(apiKey: String, // baseURL: URL = URL(string: "https://models.manashiki.com")!, // transport: any HTTPTransport = URLSessionTransport(), // storageDirectory: URL? = nil) throws
ManashikiClient is a Sendable struct, so one
instance can be shared across tasks. HTTPTransport is a
protocol; inject a mock transport in unit tests.
4. List and inspect models
// func listModels() async throws -> [ModelEntry] (GET /api/models)
let entries = try await client.listModels()
for entry in entries {
print(entry.modelId, entry.version, entry.size, entry.sha256)
}
// func getModel(_ modelId: String) async throws -> ModelRecord
// (GET /api/models/:modelId)
let record = try await client.getModel("whisper-medical-ja-fp16")
print(record.lifecycle.status, record.compatibility.minOs ?? "-") ModelEntry carries modelId,
version, size, and sha256.
ModelRecord adds the full registry record: model card,
lineage, format, compatibility (platforms, min_os, RAM /
disk requirements), lifecycle status
(draft / staging / production / deprecated / sunset),
publishing mode, and optional benchmark metrics. There is no
client-side filtering parameter on listModels() yet —
filter the returned array, e.g. on
record.compatibility.platforms.
5. Download with progress and SHA-256 verification
// func downloadModel(
// _ modelId: String,
// session: String? = nil,
// progress: ((Int64, Int64?) -> Void)? = nil
// ) async throws -> DownloadedModel
let model = try await client.downloadModel("whisper-medical-ja-fp16") { written, total in
if let total {
print("\(written) / \(total) bytes")
}
}
print(model.localURL) // .../Application Support/ManashikiModels/whisper-medical-ja-fp16.bin
print(model.sha256) // hex digest verified against the registry What happens under the hood:
GET /api/download/:modelIdreturns a short-lived signed URL (downloadUrl,sha256,size,expiresIn).- The artifact is streamed to disk in 1 MiB chunks; the progress closure receives positional
(bytesWritten, totalBytes?)values —totalBytesisnilwhen the server sends no content length. - The file's SHA-256 is computed in streaming 1 MiB chunks (never loading the whole artifact into memory) and compared with the registry digest. A mismatch throws
APIError.integrityMismatch(expected:actual:). Verification is skipped only if the server returns no digest.
Note: the mirror README sketches an older planned API
(progress.bytesWritten, downloadPrivateModel).
The shipped API is the one above — private assets use the same
downloadModel with the session: parameter.
Private (org-private) models
// 1. Your backend, holding a legacy sk_live_* server key, mints a session
// token: client.createSession(principalId:ttlSec:) → POST /api/session
// (the worker does not yet accept mk_live_*/mk_svc_* for session minting)
// 2. The app passes the token; the SDK adds it as
// Authorization: Bearer <token> on GET /api/download/:modelId.
let session = try await myBackend.fetchModelSession()
let lora = try await client.downloadModel(
"doctor-abc123-lora-whisper-decoder-v3",
session: session
)
Requesting a private asset without a session token fails with
APIError.sessionRequired.
6. Where models are stored
- Default directory:
Application Support/ManashikiModels/in the user domain, created on first use (falls back to the temporary directory if Application Support is unavailable). - Each model is written as
<modelId>.bininside that directory. - Re-downloading a model removes the existing file and writes a fresh copy.
- Pass
storageDirectory:to the initializer to use a custom location (e.g. an app-group container).
7. Errors worth handling
| Case | When |
|---|---|
invalidAPIKey / missingAPIKey | Malformed key (thrown locally at init) or rejected / absent key server-side. |
modelNotFound(String) | Unknown model id. |
modelSunset(replacement:) | Model past sunset; carries the replacement model id when one exists. |
sessionRequired | Private asset requested without a session token. |
forbiddenScope / forbiddenOrg | Key lacks the scope, or the model belongs to another org. |
rateLimited(retryAfter:) | Rate limit hit; retryAfter is in seconds when the server provides it. |
integrityMismatch(expected:actual:) | Local SHA-256 does not match the registry digest. |
http / transport | Any other HTTP status or networking failure. |
8. Restoring after reinstall
Models are content, not purchases. Entitlement — first-party catalog
access, your org's private namespace, community models — is evaluated
server-side against your org's API key (plus session token for private
assets) on every GET /api/download/:modelId. The SDK keeps
no purchase state on the device and there is nothing to restore through
StoreKit: after a reinstall, simply call downloadModel
again and the storage directory is recreated. Billing-wise,
mk_test_* downloads count against the staging quota and
mk_live_* / mk_svc_* downloads are metered
as production DLs; mk_pub_* resolutions are rate-limited
and audit-logged but not currently billed.
9. End-to-end SwiftUI example
import SwiftUI
import Manashiki
@MainActor @Observable
final class CatalogStore {
var models: [ModelEntry] = []
var progress: Double = 0
var status = ""
// Placeholder parses (random segment must be 20+ chars) — swap in your key.
private let client = try! ManashikiClient(apiKey: "mk_pub_acme_xxxxxxxxxxxxxxxxxxxxxxxx")
func load() async {
do { models = try await client.listModels() }
catch { status = "Catalog error: \(error)" }
}
func download(_ modelId: String) async {
status = "Downloading \(modelId)..."
do {
let model = try await client.downloadModel(modelId) { written, total in
guard let total else { return }
Task { @MainActor in self.progress = Double(written) / Double(total) }
}
status = "Verified \(model.sha256.prefix(8)) at \(model.localURL.lastPathComponent)"
} catch APIError.integrityMismatch(let expected, let actual) {
status = "Integrity mismatch: expected \(expected.prefix(8)), got \(actual.prefix(8))"
} catch APIError.rateLimited(let retry) {
status = "Rate limited, retry in \(retry ?? 60)s"
} catch {
status = "Download failed: \(error)"
}
}
}
struct CatalogView: View {
@State private var store = CatalogStore()
var body: some View {
List(store.models, id: \.modelId) { entry in
Button(entry.modelId) {
Task { await store.download(entry.modelId) }
}
}
.task { await store.load() }
.safeAreaInset(edge: .bottom) {
VStack {
ProgressView(value: store.progress)
Text(store.status).font(.caption)
}.padding()
}
}
} Swap the placeholder for your org's publishable key from the dashboard, then pick a model id from the catalog.