Download and cache models
Download GGUF and LiteRT-LM models from Hugging Face or HTTP(S) with progress, retry and cancel, choose where the cache lives on each platform, and inspect or clean it.
On this page
On native targets, loadModelSource(...) downloads a remote ModelSource into
a package-managed cache, verifies it, and loads the cached local file. Later
loads reuse the cached file without a network request. Loading itself is
covered in Model lifecycle.
Show download progress in an app#
ModelDownloadController wraps any ModelDownloadManager and emits UI-ready
snapshots: a stage, a progress fraction, cancel and retry, and an error message
with URL query strings and fragments redacted. It does not depend on Flutter.
final controller = ModelDownloadController(
manager: DefaultModelDownloadManager.auto(
appPrivateCacheDirectory: appCacheModelsDirectory,
),
);
final subscription = controller.snapshots.listen((snapshot) {
switch (snapshot.stage) {
case ModelDownloadTaskStage.checkingCache:
print('Checking cache for ${snapshot.source?.displayName}');
break;
case ModelDownloadTaskStage.downloading:
final percent = snapshot.fraction == null
? 'unknown'
: '${(snapshot.fraction! * 100).toStringAsFixed(1)}%';
print('Downloading $percent');
break;
case ModelDownloadTaskStage.ready:
print('Ready at ${snapshot.entry?.filePath}');
break;
case ModelDownloadTaskStage.failed:
print(snapshot.errorMessage);
break;
case ModelDownloadTaskStage.cancelled:
print('Cancelled; retry is available: ${snapshot.canRetry}');
break;
default:
break;
}
});
try {
final entry = await controller.start(
ModelSource.parse('hf://owner/repo/model-Q4_K_M.gguf'),
options: ModelLoadOptions(maxRetries: 3),
);
await engine.loadModel(entry.filePath);
} catch (_) {
if (controller.snapshot.canRetry) {
// Wire this to a Retry button.
await controller.retry();
}
} finally {
await subscription.cancel();
await controller.dispose();
}
-
Stages:
idle,resolving,checkingCache,downloading,verifying,ready,failed,cancelled. The cache check only drives UI state;readyfollows the manager's ownensureModel(...)validation and any checksum. -
cancel()requests cooperative cancellation.retry()afterfailedorcancelledreuses the last source and options. -
The controller owns cancellation: call
controller.cancel()and leaveModelLoadOptions.cancelTokenunset, orstart(...)throws. -
On web, pass a custom manager for browser storage; the default manager's
operations throw
LlamaUnsupportedExceptionthere.
Hugging Face hf:// references#
hf://owner/repo/path/to/file names one .gguf or .litertlm file and
resolves to https://huggingface.co/owner/repo/resolve/<revision>/path/to/file
with download=true. The revision defaults to main. The cache key uses the
stable hf:// identity, not the resolved URL.
final main = ModelSource.parse(
'hf://unsloth/Qwen3.5-0.8B-GGUF/Qwen3.5-0.8B-Q4_K_M.gguf',
);
final litert = ModelSource.parse(
'hf://litert-community/gemma-4-E2B-it-litert-lm/gemma-4-E2B-it.litertlm',
);
final tagged = ModelSource.parse(
'hf://owner/repo@v1.0.0/model-Q4_K_M.gguf',
);
// Use ?revision= when the revision contains `/`, such as PR refs.
final pullRequestRef = ModelSource.parse(
'hf://owner/repo/model-Q4_K_M.gguf?revision=refs/pr/12',
);
// The same pieces, built from app state.
final source = ModelSource.huggingFace(
repoId: 'owner/repo',
revision: 'main',
filePath: 'model-Q4_K_M.gguf',
);
Keep the real file extension in the path: LlamaBackend() routes by it.
Private and gated repositories#
Pass credentials through ModelLoadOptions, never in the source string:
await engine.loadModelSource(
ModelSource.parse('hf://owner/private-repo/model-Q4_K_M.gguf'),
options: ModelLoadOptions(bearerToken: hfToken),
);
Bearer tokens and custom headers go only on download requests. They are not
part of ModelSource.canonicalKey, cache metadata or toString().
Signed HTTP(S) URLs differ: canonicalKey keeps the full URL, and cacheKey
hashes it so distinct signed URLs stay distinct. Cache metadata and
toString() redact the query string, fragment and userinfo, but do not log or
persist canonicalKey for a signed URL.
Download options#
final cancelToken = ModelDownloadCancelToken();
final engine = LlamaEngine(
LlamaBackend(),
modelDownloadManager: DefaultModelDownloadManager.auto(
appPrivateCacheDirectory: appCacheModelsDirectory,
),
);
await engine.loadModelSource(
ModelSource.url(Uri.parse('https://example.com/model.gguf')),
options: ModelLoadOptions(
cachePolicy: ModelCachePolicy.preferCached,
sha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
bearerToken: hfToken,
cancelToken: cancelToken,
resume: true,
maxRetries: 3,
),
onProgress: (progress) {
final fraction = progress.fraction;
if (fraction != null) {
print('download progress: ${(fraction * 100).toStringAsFixed(1)}%');
}
},
);
ModelLoadOptions.defaults is preferCached, resume: true and
maxRetries: 3.
ModelSource.path(...) loads apply only sha256 and cancellation. A local
source with a non-default cache policy, cacheDirectory, auth headers,
resume: false or a non-default maxRetries throws
LlamaUnsupportedException instead of ignoring the option.
Cache policies#
preferCached(default): reuse a completed cache entry; otherwise download.refresh: download again and replace the cached file atomically.cacheOnly: throw without a network request when the entry is missing.-
noCache: download to a temporary entry that later loads do not reuse. Callremove(entry.cacheKey)orclear()when done, or useprune(...).
Choose the cache location#
DefaultModelDownloadManager.auto(...) keeps one call site for every
platform: desktop and server use a per-user shared cache; Android and iOS use
the app-private directory you pass.
// Desktop/server shared cache; app-private directory on Android/iOS.
final crossPlatformManager = DefaultModelDownloadManager.auto(
appPrivateCacheDirectory: appCacheModelsDirectory,
);
// Desktop/server: per-user cache shared by llamadart apps.
final desktopManager = DefaultModelDownloadManager.sharedCache();
// Mobile: an app-private directory resolved by the app.
final mobileManager = DefaultModelDownloadManager.appPrivate(
cacheDirectory: appCacheModelsDirectory,
);
// Android sharing across developers: only a directory the user granted.
final androidUserLibrary = DefaultModelDownloadManager.userSelected(
cacheDirectory: userGrantedModelLibraryDirectory,
);
// iOS/macOS sharing between apps in the same App Group.
final appGroupLibrary = DefaultModelDownloadManager.appGroup(
cacheDirectory: appGroupModelsDirectory,
);
In Flutter, resolve the mobile directory with path_provider:
getApplicationCacheDirectory() for re-downloadable models, or
getApplicationSupportDirectory() only when the app manages its backup
policy. Pass the result as appPrivateCacheDirectory, or pass
androidAppPrivateCacheDirectory and iosAppPrivateCacheDirectory
to resolve
both up front. Without one, auto(...) falls back to
Directory.systemTemp/llamadart/models, which the OS may clear.
| Platform | Default root |
|---|---|
| Linux |
$XDG_CACHE_HOME/llamadart/models
, or
$HOME/.cache/llamadart/models
when
XDG_CACHE_HOME
is unset
|
| macOS | $HOME/Library/Caches/llamadart/models |
| Windows |
%LOCALAPPDATA%\llamadart\models
, then
%APPDATA%\llamadart\models
, then
%USERPROFILE%\AppData\Local\llamadart\models
|
| Android/iOS | the supplied app-private directory, else Directory.systemTemp/llamadart/models |
-
Pass
namespace: 'your.namespace'toauto(...)orsharedCache(...)to replace thellamadartsegment, orcacheDirectoryto force a root. -
sharedCache()never invents a shared folder on Android or iOS: it throws withoutcacheDirectory. Android shares a model library only through a directory the user granted; iOS only within an App Group. Apps from unrelated iOS developers can load user-picked files but have no writable shared cache. -
DefaultModelDownloadManager(), whichLlamaEngineuses when you pass no manager, uses the same defaults asauto(), but falls back to the system temp directory when no home or cache directory exists;auto()andsharedCache()report an error instead. -
On web,
DefaultModelDownloadManageris a placeholder whose operations (ensureModel,listand the rest) throwLlamaUnsupportedException. Browser model caches are origin-scoped.
Inspect and clean the cache#
final manager = DefaultModelDownloadManager.auto(
appPrivateCacheDirectory: appCacheModelsDirectory,
);
final cached = await manager.list();
final entry = await manager.get(
ModelSource.parse('hf://owner/repo/model.gguf').cacheKey,
);
if (entry != null) {
await manager.remove(entry.cacheKey);
}
await manager.prune(
maxAge: const Duration(days: 30),
maxBytes: 20 * 1024 * 1024 * 1024,
);
await manager.clear();
Large downloads on mobile#
- Show progress and a cancel control, download one large GGUF at a time, and ask users to keep the app open.
- Do not cancel on every lifecycle pause: Android and iOS may let a short screen lock or app switch continue, and an eager cancel guarantees a restart.
-
Downloads are foreground Dart HTTP requests. If the OS suspends or kills the
app, the request can fail; a later session resumes from the
.partfile when resume is possible (see Reference). -
For downloads that must continue in the background, implement
ModelDownloadManagerin the app or a platform package: an Android foreground service or systemDownloadManagerwith a notification, or iOS backgroundURLSessiontasks. -
For device-level sharing, use
userSelected(...)on Android only after the user grants every participating app the same directory (do not request All Files Access by default), andappGroup(...)on iOS only for apps in the same App Group. Apps from unrelated developers should accept user-picked files through the document picker and copy them into the app cache: the loaders take file paths, not content URIs or file descriptors.
Limits#
-
hf://names one file. A multimodal model'smmprojGGUF is a separate source; see Multimodal. - Sharded GGUF files are not expanded. Pick a single-file GGUF.
-
llamadartdoes not list repository files or pick a quantization. Copy the exact path from the repository's Files and versions tab. -
URL-loading web backends accept only unauthenticated
preferCachedloads. Auth headers, checksums, cancel tokens, other cache policies,cacheDirectory,resume: falseand custom retries throwLlamaUnsupportedExceptionthere, as do local paths. Use a native target for those.
Reference: resume, locking and cache metadata#
Atomic writes. Downloads go to a .part file, which becomes the cached
model only after the HTTP stream and any SHA-256 check succeed.
Resume. A retry or resume sends an HTTP Range request only when the
partial file has a validator (ETag or Last-Modified); otherwise it
restarts from byte zero. A server that answers a Range
request with 200 OK also restarts it from byte zero.
Locking. Stable-cache downloads are serialized per cache entry within the
process, including across DefaultModelDownloadManager instances that share a
cache root. A same-entry caller waits for the active operation; cache-reusing
policies then re-check the cache. Different entries download in parallel.
Concurrent refresh calls each refresh in turn. noCache downloads are not
coalesced. Cancelling a waiting caller takes effect after the active operation
finishes and does not cancel it; cancelling the active download releases the
lock so a later caller can retry or resume from a safe .part file.
Metadata. Each entry has a versioned metadata.json sidecar next to the
model file. An entry is reused only when the sidecar matches the cache key,
file name and file path, and the file matches the recorded length and any
supplied or stored SHA-256. If the file is intact but the sidecar is missing,
malformed or from an unsupported schema version, the manager rebuilds the
sidecar without a network request, so cacheOnly survives metadata damage. A
missing file or a failed length or checksum check counts as a cache miss, and
cache-reusing policies download again.