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.

RequirementValue
iOS17+
macOS14+
visionOS1+
tvOS17+
Swift tools5.9
Dependencyswift-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:

PrefixTierWhere it belongs
mk_pub_<org>_…clientApp bundles. The only embeddable mk_* kind.
mk_test_<org>_…server (test env)Staging / dev tooling. Keep out of distributed builds.
mk_live_<org>_…serverYour production backend only.
mk_svc_<org>_…serverAgents 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:

  1. GET /api/download/:modelId returns a short-lived signed URL (downloadUrl, sha256, size, expiresIn).
  2. The artifact is streamed to disk in 1 MiB chunks; the progress closure receives positional (bytesWritten, totalBytes?) values — totalBytes is nil when the server sends no content length.
  3. 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

7. Errors worth handling

CaseWhen
invalidAPIKey / missingAPIKeyMalformed 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.
sessionRequiredPrivate asset requested without a session token.
forbiddenScope / forbiddenOrgKey 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 / transportAny 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.