Python · 3693 bytes Raw Blame History
1 """Typed errors for the GGUF export pipeline.
2
3 The export path shells out to llama.cpp's `convert_hf_to_gguf.py`,
4 `convert_lora_to_gguf.py`, and `llama-quantize`. Each subprocess has
5 its own failure mode; we wrap them in typed errors so the CLI can
6 surface the right remediation text without scraping stderr.
7 """
8
9 from __future__ import annotations
10
11
12 class ExportError(Exception):
13 """Base for `dlm.export` errors."""
14
15
16 class VendoringError(ExportError):
17 """`vendor/llama.cpp` submodule is missing, uninitialized, or unbuilt.
18
19 Typical remediation: `git submodule update --init --recursive` plus
20 a build step. The message should point the user at
21 `scripts/bump-llama-cpp.sh` for the canonical setup.
22 """
23
24
25 class PreflightError(ExportError):
26 """A compatibility probe failed before any subprocess launched.
27
28 Carries the offending probe name + detail so the CLI can print
29 actionable text. Unlike `VendoringError`, the remedy is data
30 (pick a different quant, fix the tokenizer) rather than tooling.
31 """
32
33 def __init__(self, probe: str, detail: str) -> None:
34 super().__init__(f"preflight failed [{probe}]: {detail}")
35 self.probe = probe
36 self.detail = detail
37
38
39 class UnsafeMergeError(ExportError):
40 """`--merged` on a QLoRA adapter without `--dequantize` (pitfall #3).
41
42 Merging LoRA deltas into a 4-bit-quantized base loses precision
43 silently; we refuse until the user confirms. Message includes the
44 exact flag to pass to proceed safely.
45 """
46
47
48 class SubprocessError(ExportError):
49 """A vendored tool exited non-zero or timed out.
50
51 Captures `cmd`, `returncode`, and `stderr` tail so the CLI can
52 show the user what actually went wrong without dumping the whole
53 subprocess output.
54 """
55
56 def __init__(
57 self,
58 *,
59 cmd: list[str],
60 returncode: int | None,
61 stderr_tail: str,
62 ) -> None:
63 suffix = f" (exit {returncode})" if returncode is not None else " (timed out)"
64 super().__init__(f"subprocess failed{suffix}: {cmd[0]!r}\n{stderr_tail}")
65 self.cmd = list(cmd)
66 self.returncode = returncode
67 self.stderr_tail = stderr_tail
68
69
70 class ExportManifestError(ExportError):
71 """`export_manifest.json` is unreadable, mis-shaped, or checksum drift."""
72
73
74 class UnknownExportTargetError(ExportError):
75 """Unknown `dlm export --target` value."""
76
77 def __init__(self, name: str, *, available: tuple[str, ...]) -> None:
78 listing = ", ".join(available)
79 super().__init__(f"unknown export target {name!r}; available targets: {listing}.")
80 self.name = name
81 self.available = available
82
83
84 class TargetSmokeError(ExportError):
85 """A runtime-target smoke check failed to start or answer correctly."""
86
87
88 class ProcessorLoadError(ExportError):
89 """HF-snapshot export couldn't load the processor for a VL/audio base.
90
91 Ships the recipient-unloadable state before network/license/cache
92 problems surface at inference. The dispatcher raises this instead
93 of swallowing the heavy exception so the CLI prints one crisp
94 remediation banner.
95 """
96
97
98 class VlGgufUnsupportedError(ExportError):
99 """VL GGUF emission refused before any subprocess launched.
100
101 Raised when the arch-probe verdict is not SUPPORTED, when
102 ``plan.merged`` is False (VL v1 is merged-only — see `vl_gguf.py`
103 module docstring), or when the adapter targets vision-tower
104 modules that upstream's converter silently drops. The dispatcher
105 catches this alongside :class:`VendoringError` and falls back to
106 HF-snapshot with a banner — the refusal is data-shaped, not a
107 subprocess failure.
108 """