"""Typed errors for the GGUF export pipeline. The export path shells out to llama.cpp's `convert_hf_to_gguf.py`, `convert_lora_to_gguf.py`, and `llama-quantize`. Each subprocess has its own failure mode; we wrap them in typed errors so the CLI can surface the right remediation text without scraping stderr. """ from __future__ import annotations class ExportError(Exception): """Base for `dlm.export` errors.""" class VendoringError(ExportError): """`vendor/llama.cpp` submodule is missing, uninitialized, or unbuilt. Typical remediation: `git submodule update --init --recursive` plus a build step. The message should point the user at `scripts/bump-llama-cpp.sh` for the canonical setup. """ class PreflightError(ExportError): """A compatibility probe failed before any subprocess launched. Carries the offending probe name + detail so the CLI can print actionable text. Unlike `VendoringError`, the remedy is data (pick a different quant, fix the tokenizer) rather than tooling. """ def __init__(self, probe: str, detail: str) -> None: super().__init__(f"preflight failed [{probe}]: {detail}") self.probe = probe self.detail = detail class UnsafeMergeError(ExportError): """`--merged` on a QLoRA adapter without `--dequantize` (pitfall #3). Merging LoRA deltas into a 4-bit-quantized base loses precision silently; we refuse until the user confirms. Message includes the exact flag to pass to proceed safely. """ class SubprocessError(ExportError): """A vendored tool exited non-zero or timed out. Captures `cmd`, `returncode`, and `stderr` tail so the CLI can show the user what actually went wrong without dumping the whole subprocess output. """ def __init__( self, *, cmd: list[str], returncode: int | None, stderr_tail: str, ) -> None: suffix = f" (exit {returncode})" if returncode is not None else " (timed out)" super().__init__(f"subprocess failed{suffix}: {cmd[0]!r}\n{stderr_tail}") self.cmd = list(cmd) self.returncode = returncode self.stderr_tail = stderr_tail class ExportManifestError(ExportError): """`export_manifest.json` is unreadable, mis-shaped, or checksum drift.""" class UnknownExportTargetError(ExportError): """Unknown `dlm export --target` value.""" def __init__(self, name: str, *, available: tuple[str, ...]) -> None: listing = ", ".join(available) super().__init__(f"unknown export target {name!r}; available targets: {listing}.") self.name = name self.available = available class TargetSmokeError(ExportError): """A runtime-target smoke check failed to start or answer correctly.""" class ProcessorLoadError(ExportError): """HF-snapshot export couldn't load the processor for a VL/audio base. Ships the recipient-unloadable state before network/license/cache problems surface at inference. The dispatcher raises this instead of swallowing the heavy exception so the CLI prints one crisp remediation banner. """ class VlGgufUnsupportedError(ExportError): """VL GGUF emission refused before any subprocess launched. Raised when the arch-probe verdict is not SUPPORTED, when ``plan.merged`` is False (VL v1 is merged-only — see `vl_gguf.py` module docstring), or when the adapter targets vision-tower modules that upstream's converter silently drops. The dispatcher catches this alongside :class:`VendoringError` and falls back to HF-snapshot with a banner — the refusal is data-shaped, not a subprocess failure. """