File size: 8,649 Bytes
d87bc64
7197abd
5426482
ac94e67
 
d87bc64
7197abd
5426482
 
ac94e67
5426482
d87bc64
5426482
 
 
7197abd
d87bc64
 
5426482
 
 
d87bc64
 
 
7197abd
d87bc64
 
 
 
 
7197abd
d87bc64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7197abd
d87bc64
 
385ed94
 
 
 
 
 
 
 
 
 
 
 
 
 
d87bc64
 
 
 
 
 
 
 
 
 
 
 
7197abd
 
d87bc64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aaef90f
 
 
 
 
 
 
d87bc64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aaef90f
d87bc64
 
 
 
 
aaef90f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d87bc64
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#!/usr/bin/env bash
# Thanatos-27B β€” heal a previously pulled HF-bridge tag whose bundled
# GGUF is `qwen36`-stamped (legacy v0.6.0-era pulls before `964e418`,
# 3rd-round-trip-era pulls between `973d7ef` and `978798f`, or
# 5th-round-trip-era pulls between `ae67ed1` and `e03e10e`).
#
# Fresh pulls of `ollama run hf.co/FoolDev/Thanatos-27B` now get the
# qwen35-stamped bundle and load directly β€” this script is the
# recovery path for users who pulled a qwen36-stamped blob into
# their local Ollama store during one of the qwen36 windows
# and haven't refreshed since.
#
# It rebadges the HF-bridge tag's model blob in-place (qwen36 ->
# qwen35, metadata-only, byte-identical tensors) and rewrites the
# manifest's model-layer digest to point at the new blob. After
# running, the cached `hf.co/FoolDev/Thanatos-27B` tag loads.
#
# Idempotent: a tag already on qwen35 / qwen35moe is left untouched.
# The current bundle is qwen35-stamped so this script is a no-op for
# anyone who pulled after the latest re-stamp; it stays in the repo
# for the legacy recovery case.
#
# Usage:
#   ./scripts/heal_hf_pull.sh                                # default tag
#   TAG=hf.co/FoolDev/Thanatos-27B:Q4_K_M ./scripts/heal_hf_pull.sh
#
# Requires: ollama, jq, python3 with the `gguf` package, sha256sum.
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TAG="${TAG:-hf.co/FoolDev/Thanatos-27B:Q4_K_M}"
OLLAMA_MODELS="${OLLAMA_MODELS:-${HOME}/.ollama/models}"

red()   { printf "\033[31m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; }
blue()  { printf "\033[34m%s\033[0m\n" "$*"; }

blue "[*] tag:    ${TAG}"
blue "[*] store:  ${OLLAMA_MODELS}"

# ---- 1. Sanity ---------------------------------------------------------------

for bin in ollama jq python3 sha256sum; do
    if ! command -v "${bin}" >/dev/null 2>&1; then
        red "[!] missing dependency: ${bin}"; exit 1
    fi
done

# ---- 2. Locate the model blob and manifest ----------------------------------

# `ollama show --modelfile` writes a FROM line with the absolute blob path.
# Reliable regardless of which case variant the user pulled with
# (hf.co's 307 lets `Thanatos-27B` and `thanatos-27b` both resolve to the
# canonical repo, and ollama stores the manifest under whichever case
# was first registered).
#
# The `if MODELFILE_OUT=...; then` form rather than a direct command
# substitution is load-bearing: under `set -e + pipefail`, a bare
# `MODEL_BLOB="$(ollama show ... | awk ...)"` silently terminates
# the script when the tag isn't pulled (ollama show exits non-zero
# -> pipefail propagates -> set -e exits the script before the
# explicit `[[ -z "${MODEL_BLOB}" ]]` check below ever runs). The
# `if` form takes the false branch on failure without tripping
# set -e, so the user gets the actionable error instead of a silent
# `make: *** [Makefile:N: heal-hf] Error 1`.
MODEL_BLOB=""
if MODELFILE_OUT="$(ollama show --modelfile "${TAG}" 2>/dev/null)"; then
    MODEL_BLOB="$(awk '/^FROM[[:space:]]/ {print $2; exit}' <<<"${MODELFILE_OUT}")"
fi
if [[ -z "${MODEL_BLOB}" || ! -f "${MODEL_BLOB}" ]]; then
    red "[!] could not resolve model blob for tag '${TAG}'."
    red "    Is the tag pulled? Try: ollama pull ${TAG}"
    exit 1
fi
MODEL_HASH="$(basename "${MODEL_BLOB}" | sed 's/^sha256-//')"
blue "[*] blob:   ${MODEL_BLOB}"

# Find the manifest by grepping for the model digest. The blob is
# referenced from exactly one tag in the heal scenario β€” fresh HF pull
# of a single :Q4_K_M tag β€” but if someone has multiple tags pointing
# at the same blob, we filter down to the one matching ${TAG}.
TAG_PATH="${TAG#hf.co/}"      # FoolDev/Thanatos-27B:Q4_K_M
NAMESPACE_PATH="${TAG_PATH%:*}" # FoolDev/Thanatos-27B
TAG_FILE="${TAG_PATH##*:}"    # Q4_K_M

MANIFEST="$(find "${OLLAMA_MODELS}/manifests/hf.co" \
              -type f \
              -ipath "*/${NAMESPACE_PATH}/${TAG_FILE}" 2>/dev/null | head -1)"

if [[ -z "${MANIFEST}" || ! -f "${MANIFEST}" ]]; then
    red "[!] manifest not found under ${OLLAMA_MODELS}/manifests/hf.co for tag '${TAG}'."
    exit 1
fi
blue "[*] manifest: ${MANIFEST}"

# ---- 3. Inspect arch ---------------------------------------------------------

ARCH="$(python3 - "${MODEL_BLOB}" <<'PY'
import sys
from gguf import GGUFReader, constants
r = GGUFReader(sys.argv[1], "r")
f = r.get_field(constants.Keys.General.ARCHITECTURE)
print(bytes(f.parts[f.data[0]]).decode())
PY
)"
blue "[*] arch:    ${ARCH}"

if [[ "${ARCH}" == "qwen35" || "${ARCH}" == "qwen35moe" ]]; then
    green "[=] already on a loadable arch (${ARCH}) β€” nothing to heal."
    exit 0
fi
if [[ "${ARCH}" != "qwen36" ]]; then
    red "[!] unexpected arch '${ARCH}' β€” refusing to heal. Edit this script if intentional."
    exit 1
fi

# ---- 4. Rebadge to a temp blob and stage it in the store --------------------

# Stage in the repo's .cache/ rather than /tmp: the rebadged copy is the same
# size as the original (~17 GB), which blows past a typical tmpfs /tmp budget.
# .cache/ is on the same filesystem as ~/.ollama on a normal Linux home dir
# layout, so the final move into blobs/ is an atomic rename, not a copy.
SCRATCH_DIR="${ROOT}/.cache"
mkdir -p "${SCRATCH_DIR}"
TMP_BLOB="$(mktemp -p "${SCRATCH_DIR}" thanatos-heal.XXXXXX.gguf)"
trap 'rm -f "${TMP_BLOB}"' EXIT
blue "[*] rebadging qwen36 -> qwen35 (metadata only, tensors byte-identical) ..."
python3 "${ROOT}/scripts/rename_arch.py" \
    --from-arch qwen36 --to-arch qwen35 \
    "${MODEL_BLOB}" "${TMP_BLOB}"

NEW_HASH="$(sha256sum "${TMP_BLOB}" | awk '{print $1}')"
NEW_SIZE="$(stat -c '%s' "${TMP_BLOB}")"
NEW_BLOB="${OLLAMA_MODELS}/blobs/sha256-${NEW_HASH}"
blue "[*] new digest: sha256:${NEW_HASH}"
blue "[*] new size:   ${NEW_SIZE}"

if [[ -f "${NEW_BLOB}" ]]; then
    blue "[=] target blob already in store β€” reusing."
    rm -f "${TMP_BLOB}"
else
    mv "${TMP_BLOB}" "${NEW_BLOB}"
fi
trap - EXIT

# ---- 5. Rewrite the manifest's model layer ----------------------------------

# Keep a sidecar copy of the original manifest so we can roll back if
# `ollama show` rejects the rewrite. The blob cleanup in step 7 only
# happens AFTER validation passes, so a rollback always lands on a
# consistent (original manifest, original blob still present) state.
BACKUP_MANIFEST="${MANIFEST}.heal-backup"
cp "${MANIFEST}" "${BACKUP_MANIFEST}"

TMP_MANIFEST="$(mktemp -t thanatos-heal-manifest.XXXXXX.json)"
trap 'rm -f "${TMP_MANIFEST}"' EXIT
jq --arg new "sha256:${NEW_HASH}" \
   --argjson size "${NEW_SIZE}" '
    .layers |= map(
        if .mediaType == "application/vnd.ollama.image.model"
        then .digest = $new | .size = $size
        else .
        end
    )
' "${MANIFEST}" > "${TMP_MANIFEST}"

NEW_DIGEST_IN_MANIFEST="$(jq -r '
    .layers[] | select(.mediaType == "application/vnd.ollama.image.model") | .digest
' "${TMP_MANIFEST}")"
if [[ "${NEW_DIGEST_IN_MANIFEST}" != "sha256:${NEW_HASH}" ]]; then
    red "[!] manifest rewrite failed (digest mismatch); not committing."
    rm -f "${BACKUP_MANIFEST}"
    exit 1
fi
mv "${TMP_MANIFEST}" "${MANIFEST}"
trap - EXIT

# ---- 6. Validate the rewritten manifest via ollama show ---------------------

# `ollama show` parses the manifest, walks the layer digests, and reads
# enough of the model blob to extract metadata (arch, params, etc.).
# A buggy rewrite that produces jq-accepted JSON but breaks an ollama
# invariant (layer order, mediaType set, digest-vs-blob-bytes
# mismatch) trips here, before we lose the rollback path by removing
# the old qwen36 blob. On failure we restore from BACKUP_MANIFEST.
blue "[*] validating rewritten manifest with 'ollama show'..."
if ! ollama show "${TAG}" >/dev/null 2>&1; then
    red "[!] ollama show ${TAG} failed after the manifest rewrite."
    blue "[*] rolling back: restoring original manifest from ${BACKUP_MANIFEST}"
    mv "${BACKUP_MANIFEST}" "${MANIFEST}"
    red "    Tag is back to its pre-heal (qwen36) state. The new qwen35"
    red "    blob is left in the store; ollama auto-prunes unreferenced"
    red "    blobs on next restart."
    exit 1
fi
rm -f "${BACKUP_MANIFEST}"
green "[+] manifest validates."

# ---- 7. Remove the old qwen36 blob if no other manifest references it -------

OLD_DIGEST="sha256:${MODEL_HASH}"
if ! grep -rlF -- "${OLD_DIGEST}" "${OLLAMA_MODELS}/manifests/" >/dev/null 2>&1; then
    blue "[*] no other manifest references the old qwen36 blob β€” removing ${MODEL_BLOB}"
    rm -f "${MODEL_BLOB}"
else
    blue "[=] old qwen36 blob still referenced by another manifest β€” leaving in place."
fi

echo
green "[+] healed. Try it:"
echo "    ollama run ${TAG}"
echo "    MODEL=${TAG} make smoke"