downloadModel method
Download + install the model. Idempotent: skips if the file already
exists and (when applicable) the SHA-256 verifies. Reports progress
0..1 via onProgress when the server returns a Content-Length.
Implementation
Future<void> downloadModel(
String modelId, {
bool allowMetered = false,
void Function(double progress)? onProgress,
}) async {
final spec = _modelSpecs[modelId];
if (spec == null) {
throw StateError('Unknown local model: "$modelId".');
}
final destPath = await _modelPath(spec);
final destFile = File(destPath);
if (await destFile.exists()) {
if (spec.sha256 == null || await _verifySha256(destFile, spec.sha256!)) {
dev.log('Model "$modelId" already installed', name: 'LocalLlm');
return;
}
dev.log(
'Model "$modelId" exists but failed SHA-256 check — re-downloading',
name: 'LocalLlm',
);
await destFile.delete();
}
final partialPath = '$destPath.partial';
final partialFile = File(partialPath);
if (await partialFile.exists()) await partialFile.delete();
await partialFile.parent.create(recursive: true);
dev.log('Downloading $modelId from ${spec.url}', name: 'LocalLlm');
await _dio.download(
spec.url,
partialPath,
onReceiveProgress: (received, total) {
if (total > 0 && onProgress != null) {
onProgress(received / total);
}
},
options: Options(
responseType: ResponseType.stream,
followRedirects: true,
),
);
if (spec.sha256 != null &&
!await _verifySha256(partialFile, spec.sha256!)) {
await partialFile.delete();
throw StateError(
'SHA-256 mismatch for "$modelId" — download corrupted, please retry.',
);
}
await partialFile.rename(destPath);
dev.log('Installed $modelId at $destPath', name: 'LocalLlm');
}