Skip to content

Commit 875adc3

Browse files
mattthanouticelina
andauthored
Add swift-huggingface article (#3213)
* Add swift-huggingface article * Add entry to _blog.yml * Add discussion of cache sharing * Bump version to 0.4.0 * Add banner image * Use `hf` CLI command Co-authored-by: célina <[email protected]> * Update discussion of swift-transformers integration * Re-apply hf CLI change Co-authored-by: célina <[email protected]> --------- Co-authored-by: célina <[email protected]>
1 parent 06c5fa3 commit 875adc3

File tree

3 files changed

+370
-1
lines changed

3 files changed

+370
-1
lines changed

_blog.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4962,4 +4962,12 @@
49624962
- Claude
49634963
- Codex
49644964
- Gemini
4965-
- agents
4965+
- agents
4966+
4967+
- local: swift-huggingface
4968+
date: Dec 5, 2025
4969+
tags:
4970+
- swift
4971+
- hub
4972+
- open-source
4973+
- community
27.9 KB
Loading

swift-huggingface.md

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
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

Comments
 (0)