| 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 |
""" |