1use super::{
2 Artifact, ArtifactKind, ArtifactLease, ModelDetails, ModelEntry, ModelIndex, Source, local,
3 store::Pending,
4};
5use crate::{
6 download,
7 model::{Checkpoint, Preparation},
8 qwen4_exp,
9 storage::Paths,
10};
11use anyhow::{Context, Result, ensure};
12use std::{
13 os::unix::fs::DirBuilderExt,
14 path::{Path, PathBuf},
15};
16
17pub struct PackOptions<'a> {
19 pub output: Option<&'a Path>,
22 pub experts: &'a [u32],
24 pub keep_source: bool,
26 pub token: Option<&'a str>,
28 pub repack: bool,
30}
31
32impl ModelIndex {
33 pub fn pack(&self, reference: &str, request: PackOptions<'_>) -> Result<ModelDetails> {
38 let PackOptions {
39 output,
40 experts,
41 keep_source,
42 token,
43 repack,
44 } = request;
45
46 ensure!(
47 !experts.is_empty() && experts.iter().all(|b| (2..=4).contains(b)),
48 "expert precisions must be 4,3,2"
49 );
50
51 let model = self.resolve(reference)?;
52
53 ensure!(
54 !matches!(
55 model.description.preparation(),
56 Preparation::Unsupported { .. }
57 ),
58 "model cannot be prepared by this engine"
59 );
60
61 let previous = self.prepared_copy(&model)?;
62 let retain_download = keep_source
63 && matches!(&model.source, Source::HuggingFace { .. })
64 && model.retained_source.is_none();
65
66 let metadata_source =
67 self.metadata_repair(&model, previous.as_ref().map(|(_, lease)| lease))?;
68
69 if let Some((artifact, lease)) = &previous {
70 let bits = super::store::precisions(&lease.path);
71 let same_output =
72 output.is_none_or(|p| p.canonicalize().ok() == lease.path.canonicalize().ok());
73
74 if !repack
75 && experts.iter().all(|b| bits.contains(b))
76 && same_output
77 && !retain_download
78 && metadata_source.is_none()
79 {
80 return self.show(reference);
81 }
82
83 ensure!(
84 output.is_none_or(|p| !p.exists()),
85 "--output must name a new directory when adding variants"
86 );
87 ensure!(artifact.ready, "previous artifact is incomplete");
88 }
89
90 let pending = self.begin(ArtifactKind::Prepared)?;
91 let mut downloaded = None;
92 let mut source_lease = None;
93
94 if let Some((artifact, lease)) = &previous {
95 if artifact.external.is_some() {
96 crate::storage::require_space(
97 &pending.lease.path,
98 super::store::bytes(&lease.path)?,
99 )?;
100 }
101
102 local::copy_store(
103 &lease.path,
104 &pending.lease.path,
105 artifact.external.is_none(),
106 )?;
107
108 copy_runtime_metadata(lease, &pending.lease.path)?;
109
110 if let Some(source) = &metadata_source {
111 qwen4_exp::pack::copy_chat_metadata(&source.path, &pending.lease.path)?;
112 }
113
114 let rebuild = super::store::rebuild_variants(&pending.lease.path, experts, repack);
116
117 remove_variants(&pending.lease.path, &rebuild)?;
118
119 qwen4_exp::pack::prepare(&pending.lease.path, None, experts)?;
120
121 if retain_download {
122 source_lease = Some(self.acquire_source(&model, token, &mut downloaded)?);
123 }
124 } else {
125 let input = self.acquire_source(&model, token, &mut downloaded)?;
126
127 qwen4_exp::pack::prepare(&input.path, Some(&pending.lease.path), experts)?;
128 check_local(&model)?;
129
130 source_lease = Some(input);
131 }
132
133 Checkpoint::open(&pending.lease.path).context("validating prepared output")?;
134 sync_files(&pending.lease.path)?;
135
136 let artifact = match output {
137 Some(path) => self.export(&pending, path)?,
138 None => pending.artifact.clone(),
139 };
140 let retained = keep_source
141 .then(|| downloaded.as_ref().map(|p: &Pending| p.artifact.clone()))
142 .flatten();
143
144 self.publish(&model, artifact, retained)?;
145 drop(source_lease);
146
147 if let Some(source) = downloaded {
148 let id = source.artifact.id.clone();
149
150 drop(source);
151
152 if !keep_source {
153 self.discard(&id)?;
154 }
155 }
156
157 let id = pending.artifact.id.clone();
158
159 drop(pending);
160
161 if output.is_some() {
162 self.discard(&id)?;
163 }
164
165 self.show(reference)
166 }
167
168 fn prepared_copy(&self, model: &ModelEntry) -> Result<Option<(Artifact, ArtifactLease)>> {
169 let Some(id) = &model.prepared else {
170 return Ok(None);
171 };
172 let catalog = self.lock()?;
173 let artifact = catalog
174 .value
175 .artifacts
176 .get(id)
177 .context("prepared artifact disappeared")?;
178
179 if !self.artifact_path(artifact).exists() {
180 return Ok(None);
181 }
182
183 Ok(Some((artifact.clone(), self.lease(artifact)?)))
184 }
185
186 fn metadata_repair(
188 &self,
189 model: &ModelEntry,
190 previous: Option<&ArtifactLease>,
191 ) -> Result<Option<ArtifactLease>> {
192 let Some(previous) = previous else {
193 return Ok(None);
194 };
195 let Some(source) = self.available_metadata(model)? else {
196 return Ok(None);
197 };
198
199 if !qwen4_exp::pack::missing_chat_metadata(&source.path, &previous.path) {
200 return Ok(None);
201 }
202
203 check_local(model)?;
204
205 Ok(Some(source))
206 }
207
208 fn available_metadata(&self, model: &ModelEntry) -> Result<Option<ArtifactLease>> {
210 if let Source::Local { path, .. } = &model.source {
211 return path
212 .is_dir()
213 .then(|| self.local_source_lease(model, path))
214 .transpose();
215 }
216
217 let Some(id) = &model.retained_source else {
218 return Ok(None);
219 };
220 let catalog = self.lock()?;
221 let artifact = catalog
222 .value
223 .artifacts
224 .get(id)
225 .context("source artifact disappeared")?;
226
227 if !self.artifact_path(artifact).is_dir() {
228 return Ok(None);
229 }
230
231 Ok(Some(self.lease(artifact)?))
232 }
233
234 fn acquire_source(
235 &self,
236 model: &ModelEntry,
237 token: Option<&str>,
238 downloaded: &mut Option<Pending>,
239 ) -> Result<ArtifactLease> {
240 if let Source::Local { path, .. } = &model.source {
241 let lease = self.local_source_lease(model, path)?;
242
243 check_local(model)?;
244
245 return Ok(lease);
246 }
247
248 if let Some(id) = &model.retained_source {
249 let catalog = self.lock()?;
250
251 if let Some(artifact) = catalog.value.artifacts.get(id)
252 && self.artifact_path(artifact).exists()
253 {
254 return self.lease(artifact);
255 }
256 }
257
258 let Source::HuggingFace {
259 repo,
260 revision,
261 endpoint,
262 } = &model.source
263 else {
264 unreachable!()
265 };
266 let mut pending = self.begin(ArtifactKind::Source)?;
267 let root = pending.lease.path.clone();
268 let paths = Paths {
269 data: root.clone(),
270 scratch: root.join("transfer"),
271 config: root.join("unused.toml"),
272 };
273 let path = download::run_at(
274 &paths,
275 download::Download {
276 repo,
277 revision,
278 token,
279 metadata_only: false,
280 },
281 Some(endpoint),
282 )?;
283 pending.artifact.entry = path.strip_prefix(&root)?.to_owned();
284 let lease = ArtifactLease::external(path);
285 *downloaded = Some(pending);
286
287 Ok(lease)
288 }
289
290 fn local_source_lease(&self, model: &ModelEntry, path: &Path) -> Result<ArtifactLease> {
291 let Some(id) = &model.retained_source else {
292 return Ok(ArtifactLease::external(path.to_owned()));
293 };
294 let catalog = self.lock()?;
295 let artifact = catalog
296 .value
297 .artifacts
298 .get(id)
299 .context("source artifact disappeared")?;
300
301 self.lease(artifact)
302 }
303
304 fn export(&self, pending: &Pending, path: &Path) -> Result<Artifact> {
305 ensure!(!path.exists(), "--output must name a new directory");
306
307 let parent = path
308 .parent()
309 .filter(|p| !p.as_os_str().is_empty())
310 .unwrap_or(Path::new("."));
311
312 crate::storage::create_private_dir(parent)?;
313 self.require_external_path(&parent.canonicalize()?)?;
314 std::fs::DirBuilder::new().mode(0o700).create(path)?;
316 crate::storage::require_space(path, super::store::bytes(&pending.lease.path)?)?;
317 local::copy_store(&pending.lease.path, path, false)?;
318 sync_files(path)?;
319
320 Ok(Artifact {
321 id: super::id(),
322 kind: ArtifactKind::Prepared,
323 external: Some(path.canonicalize()?),
324 entry: PathBuf::new(),
325 ready: true,
326 })
327 }
328
329 fn discard(&self, id: &str) -> Result<()> {
330 let mut catalog = self.lock()?;
331
332 ensure!(
333 super::store::references(&catalog.value, id) == 0,
334 "cannot discard referenced artifact"
335 );
336
337 let lease = self.lease_file(id)?;
338
339 lease
340 .try_lock()
341 .context("temporary artifact is still in use")?;
342
343 let path = self.artifact_root(id);
344
345 if path.exists() {
346 std::fs::remove_dir_all(path)?;
347 }
348
349 catalog.value.artifacts.remove(id);
350
351 catalog.save()
352 }
353}
354
355pub(super) fn check_local(model: &ModelEntry) -> Result<()> {
356 if let Source::Local { path, fingerprint } = &model.source {
357 ensure!(
358 &local::fingerprint(path)? == fingerprint,
359 "local source changed; register the changed checkpoint before packing"
360 );
361 }
362
363 Ok(())
364}
365
366fn sync_files(path: &Path) -> Result<()> {
367 for entry in std::fs::read_dir(path)? {
368 let path = entry?.path();
369
370 if path.is_file() {
371 std::fs::File::open(path)?.sync_all()?;
372 }
373 }
374
375 std::fs::File::open(path)?.sync_all()?;
376
377 Ok(())
378}
379
380fn remove_variants(path: &Path, experts: &[u32]) -> Result<()> {
382 for bits in experts.iter().filter(|&&bits| bits != 4) {
383 for name in [format!("experts{bits}.bin"), format!("manifest{bits}.json")] {
384 let file = path.join(name);
385
386 if file.exists() {
387 std::fs::remove_file(file)?;
388 }
389 }
390 }
391
392 Ok(())
393}
394
395fn copy_runtime_metadata(lease: &ArtifactLease, target: &Path) -> Result<()> {
397 let metadata = lease.runtime_path();
398
399 if metadata == lease.path {
400 return Ok(());
401 }
402
403 for name in ["config.json", "tokenizer.json"] {
404 let source = metadata.join(name);
405 let destination = target.join(name);
406
407 if source.is_file() && !destination.exists() {
408 std::fs::copy(source, destination)?;
409 }
410 }
411
412 qwen4_exp::pack::copy_chat_metadata(metadata, target)
413}