Python · 4121 bytes Raw Blame History
1 """Typed errors for the Ollama integration path."""
2
3 from __future__ import annotations
4
5
6 class OllamaError(Exception):
7 """Base for `dlm.export.ollama` errors."""
8
9
10 class OllamaBinaryNotFoundError(OllamaError):
11 """`ollama` not found on PATH or standard install locations.
12
13 Remediation: install from https://ollama.com/download, then re-run.
14 """
15
16
17 class OllamaVersionError(OllamaError):
18 """Installed Ollama is older than `OLLAMA_MIN_VERSION` (audit F16).
19
20 Carries the detected and required versions so the CLI can render a
21 specific upgrade message.
22 """
23
24 def __init__(
25 self,
26 *,
27 detected: tuple[int, int, int],
28 required: tuple[int, int, int],
29 ) -> None:
30 def _fmt(v: tuple[int, int, int]) -> str:
31 return f"{v[0]}.{v[1]}.{v[2]}"
32
33 super().__init__(
34 f"Ollama {_fmt(detected)} is below the minimum supported version "
35 f"{_fmt(required)}. Upgrade from https://ollama.com/download."
36 )
37 self.detected = detected
38 self.required = required
39
40
41 class OllamaCreateError(OllamaError):
42 """`ollama create` exited non-zero.
43
44 Captures the subprocess stdout + stderr so the CLI can surface the
45 real remediation (often "base GGUF missing" or "duplicate name").
46 """
47
48 def __init__(self, *, stdout: str, stderr: str) -> None:
49 tail = stderr.strip() or stdout.strip() or "(no output)"
50 super().__init__(f"`ollama create` failed:\n{tail}")
51 self.stdout = stdout
52 self.stderr = stderr
53
54
55 class OllamaSmokeError(OllamaError):
56 """`ollama run` produced no coherent output or exited non-zero.
57
58 Smoke failures are a hard stop for the default `dlm export` flow;
59 users who know the model works but want to skip smoke can pass
60 `--no-smoke`.
61 """
62
63 def __init__(self, *, stdout: str, stderr: str) -> None:
64 super().__init__(
65 f"smoke test failed — `ollama run` returned empty or errored:\n{stderr.strip() or stdout.strip() or '(no output)'}"
66 )
67 self.stdout = stdout
68 self.stderr = stderr
69
70
71 class ModelfileError(OllamaError):
72 """Modelfile generation or validation failed.
73
74 Typically means the adapter dir is missing tokenizer metadata the
75 Modelfile needs (stops, chat template). Training should
76 have written these; surfacing this at export is the fail-fast gate.
77 """
78
79
80 class TemplateRegistryError(OllamaError):
81 """Requested template dialect not in the registry.
82
83 Registry ships one entry per `BaseModelSpec.template` Literal value
84 (`chatml`, `gemma2`, `smollm3`, `olmo2`, `llama3`, `phi3`,
85 `phi4mini`,
86 `mistral`).
87 Unknown dialect usually means an hf:-escape-hatch base whose
88 template inference picked a dialect we haven't templated — remedy
89 is to add it to the registry.
90 """
91
92
93 class VerificationError(OllamaError):
94 """Go↔Jinja closed-loop verification detected drift.
95
96 Raised when Ollama's `prompt_eval_count` (Go template output)
97 disagrees with HuggingFace's `apply_chat_template` token count for
98 the same message set. A mismatch means the dialect's Go `.gotmpl`
99 file is out of sync with the base model's Jinja reference; the
100 remedy is to regenerate the golden via `scripts/refresh-chat-
101 template-goldens.py` and, if the delta is real, fix the template.
102 """
103
104 def __init__(
105 self,
106 *,
107 ollama_name: str,
108 hf_count: int,
109 go_count: int,
110 scenario: str | None = None,
111 ) -> None:
112 where = f" on scenario {scenario!r}" if scenario else ""
113 super().__init__(
114 f"template drift on {ollama_name}{where}: HF Jinja produced "
115 f"{hf_count} prompt tokens, Ollama Go template produced "
116 f"{go_count}. Delta: {go_count - hf_count:+d}. Regenerate "
117 "the golden via scripts/refresh-chat-template-goldens.py, "
118 "then diff the .gotmpl if the delta is real."
119 )
120 self.ollama_name = ollama_name
121 self.hf_count = hf_count
122 self.go_count = go_count
123 self.scenario = scenario