Swift SDK
Swift SDK は models.manashiki.com API をラップし、カタログの
一覧取得、署名付きダウンロード URL の解決、アーティファクトのディスクへの
ストリーミング、SHA-256 の検証までを async/await で提供します。
read-only の SwiftPM ミラー
Nozium/manashiki-models-swift
から配布しています。バグ報告・サポートは
contact@utakata-scifi.com
までお願いします。
1. SwiftPM でインストール
// Package.swift — 最初のバージョンタグが公開されるまでは main を追跡
dependencies: [
.package(url: "https://github.com/Nozium/manashiki-models-swift", branch: "main"),
]
Xcode からは File → Add Package Dependencies で
https://github.com/Nozium/manashiki-models-swift を貼り付け、
依存ルールに Branch: main を選択してください(最初の
semver タグが出たらバージョンルールが使えます)。ミラーへの PR は
branch protection でブロックされるため作成しないでください。CI が
monorepo から同期しています。
| 要件 | 値 |
|---|---|
| iOS | 17+ |
| macOS | 14+ |
| visionOS | 1+ |
| tvOS | 17+ |
| Swift tools | 5.9 |
| 依存ライブラリ | swift-crypto 3.0.0+(利用可能な環境では CryptoKit を使用) |
2. 正しい API キーを選ぶ
SDK はイニシャライザでキーのフォーマットをローカル検証し、ネットワーク
通信の前に APIError.invalidAPIKey を投げます。キーはすべての
リクエストで X-API-Key ヘッダーとして送信されます。配布
バイナリに埋め込んでよいキー種別は 1 つだけです:
| Prefix | Tier | 置き場所 |
|---|---|---|
mk_pub_<org>_… | client | アプリバンドル。埋め込み可能な唯一の mk_* 種別。 |
mk_test_<org>_… | server (test 環境) | ステージング / 開発ツール用。配布ビルドには含めない。 |
mk_live_<org>_… | server | 本番バックエンド専用。 |
mk_svc_<org>_… | server | エージェントと CI 用。 |
pk_live_* / pk_test_* | client (legacy) | 旧来のインテグレーション。引き続き利用可。 |
sk_live_* / sk_test_* | server (legacy) | アプリバイナリには絶対に含めない。 |
サーバー専用メソッド — createSession(principalId:ttlSec:)
(POST /api/session)と
createModelUpload(_:)(POST /api/uploads/model)
— を client tier のキーで呼ぶと APIError.forbiddenScope が
投げられます。漏洩した mk_pub_* キーでセッションの発行や
アーティファクトのアップロードはできません。
3. クライアントの初期化
import Manashiki // 不正なキーはネットワーク通信の前に APIError.invalidAPIKey を投げます。 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 は Sendable な struct なので、
1 つのインスタンスを複数のタスク間で共有できます。
HTTPTransport はプロトコルです。ユニットテストでは
モックの transport を注入してください。
4. モデルの一覧と詳細取得
// 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 は modelId、version、
size、sha256 を持ちます。
ModelRecord はレジストリレコードの全体を含みます: モデル
カード、lineage、フォーマット、互換性情報(プラットフォーム、
min_os、RAM / ディスク要件)、lifecycle ステータス
(draft / staging / production / deprecated / sunset)、
公開モード、任意のベンチマークメトリクスです。listModels()
にはまだフィルタリング用のパラメータがないため、返ってきた配列を
record.compatibility.platforms などでフィルタしてください。
5. 進捗付きダウンロードと SHA-256 検証
// 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 内部で行われる処理:
GET /api/download/:modelIdが短命の署名付き URL(downloadUrl、sha256、size、expiresIn)を返します。- アーティファクトは 1 MiB チャンクでディスクにストリーミングされます。progress クロージャは位置引数
(bytesWritten, totalBytes?)を受け取り、サーバーが content length を返さない場合totalBytesはnilです。 - ファイルの SHA-256 は 1 MiB チャンクのストリーミングで計算され(アーティファクト全体をメモリに載せません)、レジストリのダイジェストと比較されます。不一致なら
APIError.integrityMismatch(expected:actual:)が投げられます。検証がスキップされるのは、サーバーがダイジェストを返さない場合のみです。
注意: ミラーの README には旧計画の API
(progress.bytesWritten、downloadPrivateModel)が
記載されていますが、実際にリリースされている API は上記のものです。
private アセットも同じ downloadModel に
session: パラメータを渡して取得します。
private(org-private)モデル
// 1. legacy の sk_live_* サーバーキーを持つバックエンドがセッショントークンを
// 発行: client.createSession(principalId:ttlSec:) → POST /api/session
// (worker はセッション発行に mk_live_*/mk_svc_* をまだ受け付けません)
// 2. アプリがトークンを渡すと、SDK が GET /api/download/:modelId に
// Authorization: Bearer <token> として付与します。
let session = try await myBackend.fetchModelSession()
let lora = try await client.downloadModel(
"doctor-abc123-lora-whisper-decoder-v3",
session: session
)
セッショントークンなしで private アセットを要求すると
APIError.sessionRequired で失敗します。
6. モデルの保存先
- デフォルトのディレクトリはユーザードメインの
Application Support/ManashikiModels/で、初回利用時に作成されます(Application Support が使えない場合は一時ディレクトリにフォールバック)。 - 各モデルはそのディレクトリ内に
<modelId>.binとして書き込まれます。 - 同じモデルを再ダウンロードすると、既存ファイルを削除して新しいコピーを書き込みます。
- カスタムの保存先(app-group コンテナなど)を使うには、イニシャライザに
storageDirectory:を渡してください。
7. ハンドリングすべきエラー
| Case | 発生条件 |
|---|---|
invalidAPIKey / missingAPIKey | 不正な形式のキー(init 時にローカルで throw)、またはサーバー側でキーが拒否・欠落。 |
modelNotFound(String) | 不明なモデル id。 |
modelSunset(replacement:) | sunset 済みのモデル。代替モデルがある場合はその id を含みます。 |
sessionRequired | セッショントークンなしで private アセットを要求。 |
forbiddenScope / forbiddenOrg | キーに scope が不足、またはモデルが別 org に属している。 |
rateLimited(retryAfter:) | レート制限に到達。サーバーが返す場合、retryAfter は秒単位。 |
integrityMismatch(expected:actual:) | ローカルの SHA-256 がレジストリのダイジェストと不一致。 |
http / transport | その他の HTTP ステータスやネットワーク障害。 |
8. 再インストール後の復元
モデルは「購入物」ではなくコンテンツです。利用権 — first-party
カタログへのアクセス、自 org の private namespace、community モデル —
はすべての GET /api/download/:modelId ごとに、org の API
キー(private アセットの場合はセッショントークンも)に対して
サーバー側で評価されます。SDK はデバイス上に購入状態を保持せず、
StoreKit で復元するものはありません。再インストール後は
downloadModel を呼び直すだけで保存ディレクトリも再作成
されます。課金面では、mk_test_* のダウンロードは
ステージングのクォータに計上され、mk_live_* /
mk_svc_* のダウンロードは本番 DL としてメータリング
されます。mk_pub_* の解決はレート制限と監査ログの
対象ですが、現時点では課金されません。
9. SwiftUI のエンドツーエンド例
import SwiftUI
import Manashiki
@MainActor @Observable
final class CatalogStore {
var models: [ModelEntry] = []
var progress: Double = 0
var status = ""
// プレースホルダーは形式検証を通ります (ランダム部は 20 文字以上) — 実際のキーに差し替えてください。
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()
}
}
} プレースホルダーをダッシュボードで取得した org の publishable キーに差し替え、カタログから モデル id を選んでください。