|
| 1 | +--- |
| 2 | +title: "Introducing swift-huggingface: The Complete Swift Client for Hugging Face" |
| 3 | +thumbnail: /blog/assets/swift-huggingface/banner.png |
| 4 | +authors: |
| 5 | +- user: mattt |
| 6 | + guest: true |
| 7 | +--- |
| 8 | + |
| 9 | +Today, we're announcing [swift-huggingface](https://github.com/huggingface/swift-huggingface), |
| 10 | +a new Swift package that provides a complete client for the Hugging Face Hub. |
| 11 | + |
| 12 | +You can start using it today as a standalone package, |
| 13 | +and it will soon integrate into swift-transformers as a replacement for its current `HubApi` implementation. |
| 14 | + |
| 15 | +## The Problem |
| 16 | + |
| 17 | +When we released [swift-transformers 1.0](https://huggingface.co/blog/swift-transformers) earlier this year, |
| 18 | +we heard loud and clear from the community: |
| 19 | + |
| 20 | +- **Downloads were slow and unreliable.** |
| 21 | + Large model files (often several gigabytes) |
| 22 | + would fail partway through with no way to resume. |
| 23 | + Developers resorted to manually downloading models and bundling them with their apps — |
| 24 | + defeating the purpose of dynamic model loading. |
| 25 | +- **No shared cache with the Python ecosystem.** |
| 26 | + The Python `transformers` library stores models in `~/.cache/huggingface/hub`. |
| 27 | + Swift apps downloaded to a different location with a different structure. |
| 28 | + If you'd already downloaded a model using the Python CLI, |
| 29 | + you'd download it again for your Swift app. |
| 30 | +- **Authentication is confusing.** |
| 31 | + Where should tokens come from? |
| 32 | + Environment variables? Files? Keychain? |
| 33 | + The answer is, _"It depends"_, |
| 34 | + and the existing implementation didn't make the options clear. |
| 35 | + |
| 36 | +## Introducing swift-huggingface |
| 37 | + |
| 38 | +swift-huggingface is a ground-up rewrite focused on reliability and developer experience. |
| 39 | +It provides: |
| 40 | + |
| 41 | +- **Complete Hub API coverage** — models, datasets, spaces, collections, discussions, and more |
| 42 | +- **Robust file operations** — progress tracking, resume support, and proper error handling |
| 43 | +- **Python-compatible cache** — share downloaded models between Swift and Python clients |
| 44 | +- **Flexible authentication** — a `TokenProvider` pattern that makes credential sources explicit |
| 45 | +- **OAuth support** — first-class support for user-facing apps that need to authenticate users |
| 46 | +- **Xet storage backend support** _(Coming soon!)_ — chunk-based deduplication for significantly faster downloads |
| 47 | + |
| 48 | +Let's look at some examples. |
| 49 | + |
| 50 | +--- |
| 51 | + |
| 52 | +## Flexible Authentication with TokenProvider |
| 53 | + |
| 54 | +One of the biggest improvements is how authentication works. The `TokenProvider` pattern makes it explicit where credentials come from: |
| 55 | + |
| 56 | +```swift |
| 57 | +import HuggingFace |
| 58 | + |
| 59 | +// For development: auto-detect from environment and standard locations |
| 60 | +// Checks HF_TOKEN, HUGGING_FACE_HUB_TOKEN, ~/.cache/huggingface/token, etc. |
| 61 | +let client = HubClient.default |
| 62 | + |
| 63 | +// For CI/CD: explicit token |
| 64 | +let client = HubClient(tokenProvider: .static("hf_xxx")) |
| 65 | + |
| 66 | +// For production apps: read from Keychain |
| 67 | +let client = HubClient(tokenProvider: .keychain(service: "com.myapp", account: "hf_token")) |
| 68 | +``` |
| 69 | + |
| 70 | +The auto-detection follows the same conventions as the Python `huggingface_hub` library: |
| 71 | + |
| 72 | +1. `HF_TOKEN` environment variable |
| 73 | +2. `HUGGING_FACE_HUB_TOKEN` environment variable |
| 74 | +3. `HF_TOKEN_PATH` environment variable (path to token file) |
| 75 | +4. `$HF_HOME/token` file |
| 76 | +5. `~/.cache/huggingface/token` (standard HF CLI location) |
| 77 | +6. `~/.huggingface/token` (fallback location) |
| 78 | + |
| 79 | +This means if you've already logged in with `hf auth login`, |
| 80 | +swift-huggingface will automatically find and use that token. |
| 81 | + |
| 82 | +## OAuth for User-Facing Apps |
| 83 | + |
| 84 | +Building an app where users sign in with their Hugging Face account? |
| 85 | +swift-huggingface includes a complete OAuth 2.0 implementation: |
| 86 | + |
| 87 | +```swift |
| 88 | +import HuggingFace |
| 89 | + |
| 90 | +// Create authentication manager |
| 91 | +let authManager = try HuggingFaceAuthenticationManager( |
| 92 | + clientID: "your_client_id", |
| 93 | + redirectURL: URL(string: "yourapp://oauth/callback")!, |
| 94 | + scope: [.openid, .profile, .email], |
| 95 | + keychainService: "com.yourapp.huggingface", |
| 96 | + keychainAccount: "user_token" |
| 97 | +) |
| 98 | + |
| 99 | +// Sign in user (presents system browser) |
| 100 | +try await authManager.signIn() |
| 101 | + |
| 102 | +// Use with Hub client |
| 103 | +let client = HubClient(tokenProvider: .oauth(manager: authManager)) |
| 104 | + |
| 105 | +// Tokens are automatically refreshed when needed |
| 106 | +let userInfo = try await client.whoami() |
| 107 | +print("Signed in as: \(userInfo.name)") |
| 108 | +``` |
| 109 | + |
| 110 | +The OAuth manager handles token storage in Keychain, |
| 111 | +automatic refresh, and secure sign-out. |
| 112 | +No more manual token management. |
| 113 | + |
| 114 | +## Reliable Downloads |
| 115 | + |
| 116 | +Downloading large models is now straightforward with proper progress tracking and resume support: |
| 117 | + |
| 118 | +```swift |
| 119 | +// Download with progress tracking |
| 120 | +let progress = Progress(totalUnitCount: 0) |
| 121 | + |
| 122 | +Task { |
| 123 | + for await _ in progress.publisher(for: \.fractionCompleted).values { |
| 124 | + print("Download: \(Int(progress.fractionCompleted * 100))%") |
| 125 | + } |
| 126 | +} |
| 127 | + |
| 128 | +let fileURL = try await client.downloadFile( |
| 129 | + at: "model.safetensors", |
| 130 | + from: "microsoft/phi-2", |
| 131 | + to: destinationURL, |
| 132 | + progress: progress |
| 133 | +) |
| 134 | +``` |
| 135 | + |
| 136 | +If a download is interrupted, |
| 137 | +you can resume it: |
| 138 | + |
| 139 | +```swift |
| 140 | +// Resume from where you left off |
| 141 | +let fileURL = try await client.resumeDownloadFile( |
| 142 | + resumeData: savedResumeData, |
| 143 | + to: destinationURL, |
| 144 | + progress: progress |
| 145 | +) |
| 146 | +``` |
| 147 | + |
| 148 | +For downloading entire model repositories, |
| 149 | +`downloadSnapshot` handles everything: |
| 150 | + |
| 151 | +```swift |
| 152 | +let modelDir = try await client.downloadSnapshot( |
| 153 | + of: "mlx-community/Llama-3.2-1B-Instruct-4bit", |
| 154 | + to: cacheDirectory, |
| 155 | + matching: ["*.safetensors", "*.json"], // Only download what you need |
| 156 | + progressHandler: { progress in |
| 157 | + print("Downloaded \(progress.completedUnitCount) of \(progress.totalUnitCount) files") |
| 158 | + } |
| 159 | +) |
| 160 | +``` |
| 161 | + |
| 162 | +The snapshot function tracks metadata for each file, |
| 163 | +so subsequent calls only download files that have changed. |
| 164 | + |
| 165 | +## Shared Cache with Python |
| 166 | + |
| 167 | +Remember the second problem we mentioned? |
| 168 | +_"No shared cache with the Python ecosystem."_ |
| 169 | +That's now solved. |
| 170 | + |
| 171 | +swift-huggingface implements a Python-compatible cache structure |
| 172 | +that allows seamless sharing between Swift and Python clients: |
| 173 | + |
| 174 | +``` |
| 175 | +~/.cache/huggingface/hub/ |
| 176 | +├── models--deepseek-ai--DeepSeek-V3.2/ |
| 177 | +│ ├── blobs/ |
| 178 | +│ │ └── <etag> # actual file content |
| 179 | +│ ├── refs/ |
| 180 | +│ │ └── main # contains commit hash |
| 181 | +│ └── snapshots/ |
| 182 | +│ └── <commit_hash>/ |
| 183 | +│ └── config.json # symlink → ../../blobs/<etag> |
| 184 | +``` |
| 185 | + |
| 186 | +This means: |
| 187 | + |
| 188 | +- **Download once, use everywhere.** |
| 189 | + If you've already downloaded a model with the `hf` CLI or the Python library, |
| 190 | + swift-huggingface will find it automatically. |
| 191 | +- **Content-addressed storage.** |
| 192 | + Files are stored by their ETag in the `blobs/` directory. |
| 193 | + If two revisions share the same file, it's only stored once. |
| 194 | +- **Symlinks for efficiency.** |
| 195 | + Snapshot directories contain symlinks to blobs, |
| 196 | + minimizing disk usage while maintaining a clean file structure. |
| 197 | + |
| 198 | +The cache location follows the same environment variable conventions as Python: |
| 199 | + |
| 200 | +1. `HF_HUB_CACHE` environment variable |
| 201 | +2. `HF_HOME` environment variable + `/hub` |
| 202 | +3. `~/.cache/huggingface/hub` (default) |
| 203 | + |
| 204 | +You can also use the cache directly: |
| 205 | + |
| 206 | +```swift |
| 207 | +let cache = HubCache.default |
| 208 | + |
| 209 | +// Check if a file is already cached |
| 210 | +if let cachedPath = cache.cachedFilePath( |
| 211 | + repo: "deepseek-ai/DeepSeek-V3.2", |
| 212 | + kind: .model, |
| 213 | + revision: "main", |
| 214 | + filename: "config.json" |
| 215 | +) { |
| 216 | + let data = try Data(contentsOf: cachedPath) |
| 217 | + // Use cached file without any network request |
| 218 | +} |
| 219 | +``` |
| 220 | + |
| 221 | +To prevent race conditions when multiple processes access the same cache, |
| 222 | +swift-huggingface uses file locking |
| 223 | +([`flock(2)`](https://man7.org/linux/man-pages/man2/flock.2.html)). |
| 224 | + |
| 225 | +## Before and After |
| 226 | + |
| 227 | +Here's what downloading a model snapshot looked like with the old `HubApi`: |
| 228 | + |
| 229 | +```swift |
| 230 | +// Before: HubApi in swift-transformers |
| 231 | +let hub = HubApi() |
| 232 | +let repo = Hub.Repo(id: "mlx-community/Llama-3.2-1B-Instruct-4bit") |
| 233 | + |
| 234 | +// No progress tracking, no resume, errors swallowed |
| 235 | +let modelDir = try await hub.snapshot( |
| 236 | + from: repo, |
| 237 | + matching: ["*.safetensors", "*.json"] |
| 238 | +) { progress in |
| 239 | + // Progress object exists but wasn't always accurate |
| 240 | + print(progress.fractionCompleted) |
| 241 | +} |
| 242 | +``` |
| 243 | + |
| 244 | +And here's the same operation with swift-huggingface: |
| 245 | + |
| 246 | +```swift |
| 247 | +// After: swift-huggingface |
| 248 | +let client = HubClient.default |
| 249 | + |
| 250 | +let modelDir = try await client.downloadSnapshot( |
| 251 | + of: "mlx-community/Llama-3.2-1B-Instruct-4bit", |
| 252 | + to: cacheDirectory, |
| 253 | + matching: ["*.safetensors", "*.json"], |
| 254 | + progressHandler: { progress in |
| 255 | + // Accurate progress per file |
| 256 | + print("\(progress.completedUnitCount)/\(progress.totalUnitCount) files") |
| 257 | + } |
| 258 | +) |
| 259 | +``` |
| 260 | + |
| 261 | +The API is similar, but the implementation is completely different — |
| 262 | +built on `URLSession` download tasks with proper |
| 263 | +delegate handling, resume data support, and metadata tracking. |
| 264 | + |
| 265 | +## Beyond Downloads |
| 266 | + |
| 267 | +But wait, there's more! |
| 268 | +swift-huggingface contains a complete Hub client: |
| 269 | + |
| 270 | +```swift |
| 271 | +// List trending models |
| 272 | +let models = try await client.listModels( |
| 273 | + filter: "library:mlx", |
| 274 | + sort: "trending", |
| 275 | + limit: 10 |
| 276 | +) |
| 277 | + |
| 278 | +// Get model details |
| 279 | +let model = try await client.getModel("mlx-community/Llama-3.2-1B-Instruct-4bit") |
| 280 | +print("Downloads: \(model.downloads ?? 0)") |
| 281 | +print("Likes: \(model.likes ?? 0)") |
| 282 | + |
| 283 | +// Work with collections |
| 284 | +let collections = try await client.listCollections(owner: "huggingface", sort: "trending") |
| 285 | + |
| 286 | +// Manage discussions |
| 287 | +let discussions = try await client.listDiscussions(kind: .model, "username/my-model") |
| 288 | +``` |
| 289 | + |
| 290 | +And that's not all! |
| 291 | +swift-huggingface has everything you need to interact with |
| 292 | +[Hugging Face Inference Providers](https://huggingface.co/docs/inference-providers/index), |
| 293 | +giving your app instant access to hundreds of machine learning models, |
| 294 | +powered by world-class inference providers: |
| 295 | + |
| 296 | +```swift |
| 297 | +import HuggingFace |
| 298 | + |
| 299 | +// Create a client (uses auto-detected credentials from environment) |
| 300 | +let client = InferenceClient.default |
| 301 | + |
| 302 | +// Generate images from a text prompt |
| 303 | +let response = try await client.textToImage( |
| 304 | + model: "black-forest-labs/FLUX.1-schnell", |
| 305 | + prompt: "A serene Japanese garden with cherry blossoms", |
| 306 | + provider: .hfInference, |
| 307 | + width: 1024, |
| 308 | + height: 1024, |
| 309 | + numImages: 1, |
| 310 | + guidanceScale: 7.5, |
| 311 | + numInferenceSteps: 50, |
| 312 | + seed: 42 |
| 313 | +) |
| 314 | + |
| 315 | +// Save the generated image |
| 316 | +try response.image.write(to: URL(fileURLWithPath: "generated.png")) |
| 317 | +``` |
| 318 | + |
| 319 | +Check the [README](https://github.com/huggingface/swift-huggingface) for a full list of everything that's supported. |
| 320 | + |
| 321 | +## What's Next |
| 322 | + |
| 323 | +We're actively working on two fronts: |
| 324 | + |
| 325 | +**Integration with swift-transformers.** |
| 326 | +We have a [pull request in progress](https://github.com/huggingface/swift-transformers/pull/297) to replace `HubApi` with swift-huggingface. |
| 327 | +This will bring reliable downloads to everyone using swift-transformers, |
| 328 | +[mlx-swift-lm](https://github.com/ml-explore/mlx-swift-lm), |
| 329 | +and the broader ecosystem. |
| 330 | +If you maintain a Swift-based library or app and want help adopting swift-huggingface, reach out — we're happy to help. |
| 331 | + |
| 332 | +**Faster downloads with Xet.** |
| 333 | +We're adding support for the [Xet storage backend](https://huggingface.co/docs/hub/storage-backends), |
| 334 | +which enables chunk-based deduplication and significantly faster downloads for large models. |
| 335 | +More on this soon. |
| 336 | + |
| 337 | +## Try It Out |
| 338 | + |
| 339 | +Add swift-huggingface to your project: |
| 340 | + |
| 341 | +```swift |
| 342 | +dependencies: [ |
| 343 | + .package(url: "https://github.com/huggingface/swift-huggingface.git", from: "0.4.0") |
| 344 | +] |
| 345 | +``` |
| 346 | + |
| 347 | +We'd love your feedback. |
| 348 | +If you've been frustrated with model downloads in Swift, give this a try and |
| 349 | +[let us know how it goes](https://github.com/huggingface/swift-huggingface/issues). |
| 350 | +Your experience reports will help us prioritize what to improve next. |
| 351 | + |
| 352 | +## Resources |
| 353 | + |
| 354 | +- [swift-huggingface on GitHub](https://github.com/huggingface/swift-huggingface) |
| 355 | +- [swift-transformers](https://github.com/huggingface/swift-transformers) |
| 356 | +- [mlx-swift-examples](https://github.com/ml-explore/mlx-swift-examples) |
| 357 | +- [AnyLanguageModel](https://github.com/mattt/AnyLanguageModel) |
| 358 | + |
| 359 | +--- |
| 360 | + |
| 361 | +*Thanks to the swift-transformers community for the feedback that shaped this project, and to everyone who filed issues and shared their experiences. This is for you.* ❤️ |
0 commit comments