# Changelog All notable changes to this repository. Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The repository holds a model card, an Ollama Modelfile, the HF Ollama-bridge `template` / `system` / `params` files, tooling and the bundled Q4_K_M GGUF, so versions track the **tooling and documentation**, not the underlying base model. ## [Unreleased] - **Parked, needs a restamp:** `chat_template.jinja:225` still credits the reasoning field order to "the Janus sibling's order". The README half of that correction shipped in 0.12.5; this copy is stamped into the GGUF's `tokenizer.chat_template` and pinned by `verify_arch.py`, so fixing a comment here costs a ~16 GB re-upload and a new blob sha256 that makes every existing puller re-download. Fold it into the next change that genuinely requires a restamp. ## [0.12.17] — 2026-09-21 ### Notes - A one-line correction to 0.12.16's own correction. Replacing 0.12.15's invented quotation (below), 0.12.16 wrote that what 0.12.12 "did not exercise was the fetch-failure direction" — naming one gap where there were two. 0.12.12 recorded exercising the in-sync path, against the companion repo's identical-template `janus` tag, and the missing-tag path; the drifted direction was outside it as well. The sentence now says what that entry actually recorded. - That is the fourth consecutive release in which a correction carried a new inaccuracy. The common cause each time was writing the correction from the sibling repo's situation rather than reading this repo's own entry first. Text-only release; blob untouched. ## [0.12.16] — 2026-09-21 A third adversarial audit, run against 0.12.15, found that the two previous releases were generated from the sibling repo's text and carried its version numbers and its claims into this card. That transcription is amended in place — it is not a claim a reader could have acted on, it is this card printing the other card's words — and the false claims are corrected here in the usual way. ### Fixed - **The "would span" correction landed in the script and not in the README.** 0.12.15 declared that the header stated a hypothetical as past fact and fixed it; the identical sentence in the README — the actual model card — still read "spans". That is the third time in this sequence a fix landed in one file and not its pair. - **The drift hint's local branch dropped the tag it had just checked.** Only the `hf.co/` branch echoed `${TAG}`. Both `make` refreshes now carry `TAG=${TAG}`, which `load_bundle.sh` and `build.sh` both honour. - **The daemon probe answered a question it had not asked.** It ran `ollama list` only to test reachability, then reported "not installed" for any other `ollama show` failure. It now consults the list and reports one of three distinct causes. - **The template guard merged three causes into one message** — "missing, empty or unreadable" — the anti-pattern 0.12.14 fixed for the tag/daemon pair. Now one per message. - **The exit-2 enumeration went stale** when 0.12.15 added the unreadable-template cause. - **The README named two live-daemon targets where the script header names three.** - **0.12.15's one-word "would span" edit left the header line at 84 columns**, five past the wrap the rest of the block keeps. ### Notes - Corrections to the entries below: - **0.12.15 "corrected" a sentence this repo never published.** It quoted *"0.12.12 verified only the matching direction"* as a claim from the 0.12.13 entry. No such sentence has ever appeared here; it is the sibling's, about its own 0.9.12, and the supporting assertion about what 0.12.12 verified contradicted the 0.12.12 entry itself. Replaced with what is actually true of this repo. - **The 0.12.14 entry cited `0.9.13` twice** where it meant this repo's 0.12.13. Amended. - *"seven were fixed in 0.12.13"* (0.12.15). They were fixed in **0.12.14**; 0.12.13 is the release the audit was run *against*. Amended. - *"the only **user-facing** script without one"* (0.12.14). Still false: `strip_mtp.py` is documented for direct invocation and has no target either. Now a plain enumeration. - Long lines introduced by scripted edits in the 0.12.14 and 0.12.15 entries (up to 385 columns) are reflowed to the file's width. Text-only release; blob untouched. ## [0.12.15] — 2026-09-21 ### Fixed - **An unreadable `template` was reported as tag DRIFT.** The guard checked that the file exists and is non-empty, then compared against `$(cat "${TEMPLATE}")` — which yields an empty string when the read fails on permissions. A readable tag plus an unreadable reference therefore compared unequal, and the script told the reader their tag had drifted and to rebuild it, when nothing was wrong with the tag at all. The guard now tests readability too, and the reference is read explicitly so a failed read can never be mistaken for drift. Verified with a `chmod 000` template: exit 2, not 1. - **The script header stated a hypothetical as past fact.** It read "The equivalent gap here spans 0.12.7's tool-chain render …", describing a drift episode that happened on the companion Janus repo, not here. Now "would span". ### Notes - Unlike the sibling repo, no entry here ever claimed "0.12.12 verified only the matching direction", so there is nothing of that kind to correct. What 0.12.12 recorded is narrower than the sibling's: the in-sync path, exercised against the companion repo's identical-template `janus` tag, and the missing-tag path. The drifted direction and the fetch-failure direction were both outside it. - This closes the findings from the second adversarial audit. Of the ten distinct defects it confirmed, seven were fixed in 0.12.14 and the remainder here. Text-only release; blob untouched. ## [0.12.14] — 2026-09-21 ### Fixed - **The drift hint printed the destructive command this repo's own README warns against.** 0.12.13 added a README warning that a bare `ollama create thanatos-27b -f Modelfile` swaps a Q3/Q5/Q6 build back to the bundled Q4_K_M — and then had `check_tag_sync.sh` print exactly that command as its drift hint. The fix landed in the README only. The hint now names `make load-bundle` (path B) and `make build QUANT=` (path C) and says not to use the bare `ollama create`. - **The drift hint named a hard-coded refresh tag, not the tag just checked.** Running the check on `hf.co/…:Q4_K_M` and being told to refresh a different tag leaves the tag you actually hold stale — the same class of defect 0.12.13 fixed in the README while leaving it in the script. The hint now echoes the tag it was given, and branches on whether that tag is an `hf.co/` pull or a local build. - **"An unreadable tag and a stopped Ollama daemon no longer report the same cause" was false.** The previous release merged both into one message offering them as alternatives, which is the opposite of distinguishing them. The script now probes `ollama list` and reports exactly one: a missing tag, or an unreachable daemon. - **A doubled `# #` comment marker** at line 9 of the script header, introduced by the 0.12.13 header rewrite. Harmless to bash and shellcheck — which is why nothing caught it — but visibly wrong in shipped text. - **The rewritten subsection swallowed the blank line** before the `###` heading that follows it, leaving a heading butted against a paragraph. ### Notes - Two claims in the 0.12.13 entry below are corrected here rather than rewritten: - *"the only README-documented script in the repo without a make target."* False: `check_bridge_sync.py`, `check_go_template.py` and `strip_mtp.py` are all documented in the files table and have no target either. The true statement is narrower — it was the only script in the check/verify family without one. `check_bridge_sync.py`, `check_go_template.py` and `strip_mtp.py` have no target either, and `strip_mtp.py` is documented for direct use, so "user-facing" did not narrow it correctly. - *"`curl -s` … exits 0 on a 404, so … both sides empty."* A 404 from this host returns a 15-byte body, not nothing, so a 404 produces a false **STALE**, not a false "up to date". Only a connection or DNS failure yields the empty-vs-empty case. Both are caught by `curl -fsS` plus the empty-side guard; the rationale given for one of them was wrong. - These defects were found by a second adversarial audit, run against 0.12.13 itself. Its verification pass was still resolving when this release was cut, so further corrections may follow. Text-only release; blob untouched. ## [0.12.13] — 2026-09-21 ### Fixed - **The trailing-newline explanation was wrong, and the `awk` scrape it justified was never needed.** 0.12.11 and 0.12.12 both said "this repo's `template` has no trailing newline and `ollama show` adds one". `ollama show` adds nothing — the newline came from the `awk` in the scrape itself. `ollama show --template ` returns the template verbatim, byte-identical to the file. The README snippet and `scripts/check_tag_sync.sh` now use `--template` and compare directly; the scrape and the paragraph explaining it are gone. - **The check could report "up to date" having compared nothing.** `curl -s` is silent on a DNS failure and exits 0 on a 404, so an unreachable reference *plus* an unreadable tag left both sides empty — and equal. The snippet now uses `curl -fsS` and refuses to compare when either side is empty. - **The refresh advice named two commands for three documented paths, and one of them was destructive.** This card's Ollama section lists three paths (A `ollama run hf.co/…`, B `make load-bundle`, C `make build [QUANT=…]`); 0.12.11 carried over the companion repo's two-path sentence. Worse, the `ollama create thanatos-27b -f Modelfile` it named points `FROM` at the **bundled Q4_K_M**, so a reader who built Q3_K_M or Q5_K_M via path C would have silently swapped their quant back to Q4_K_M while "refreshing a template". All three refreshes are now named, with that substitution called out. ### Changed - `scripts/check_tag_sync.sh` honours `$TAG` as well as a positional argument, matching its siblings, and gains a **`make check-tag`** target — it was the only README-documented script in the repo without one. - An unreadable tag and a stopped Ollama daemon no longer report the same cause. ### Notes - Claims in the 0.12.11 entry below are corrected here rather than rewritten, following the sibling repo's precedent: - *"0.12.8's system-block fix … change how tool calls come out."* 0.12.8 is a **no-op** for any request carrying tools — both guards it adds are true when `.Tools` is non-empty. It changes requests with **no** `tools` array. Only 0.12.7 altered tool-call rendering. - *"every check here passed … they all run against this repo's own files."* The static checks do; `make smoke`, `make smoke-tools` and `make bench` run against the installed tag on a live daemon. None of them compared its template with this repo's. - The **"~1 s re-pull, no change in store size"** figure was measured on the companion Janus repo's tag, not here — this host has never held a Thanatos tag. The content-addressing it illustrates applies equally, but the number is not this repo's and the 0.12.11 entry should not have stated it unattributed. - The `[Ollama](#ollama)` link in the 0.12.11 entry pointed at an anchor `CHANGELOG.md` does not contain; it now resolves to `README.md#ollama`. Text-only release; blob untouched. ## [0.12.12] — 2026-09-21 ### Fixed - **0.12.11's check could not be run by the readers most likely to need it.** It ended in `$(cat template)` and told the reader to run it "from the repo root" — but a reader who pulled with `ollama run hf.co/FoolDev/Thanatos-27B-HERETIC` has no clone on disk at all. Exactly the people whose tag goes stale had nothing to compare against. The snippet now reads the template over HTTP from this repo (`.../raw/main/template`), so it works with nothing checked out. Verified against the live URL. ### Added - **`scripts/check_tag_sync.sh`** — the same check as a script, for anyone who does have a clone. It completes the pair with `check_bridge_sync.py`: that one verifies the `Modelfile` and the bridge files agree *inside* the repo, this one verifies an *installed tag* still agrees with the repo. Exit 0 in sync, 1 drifted, 2 unusable. ### Notes - The script splits `ollama show` from the `awk` filter rather than writing `HAVE="$(ollama show ... 2>/dev/null | awk ...)"` — the silent-exit footgun this repo shipped once and check.sh has guarded since. Under `set -e` + `pipefail` a missing tag aborts on the assignment and the emptiness check below never runs. - This host has no `thanatos-27b` tag, so the in-sync path was exercised against the identical-template `janus` tag from the companion repo, and the missing-tag path against the real name. Both behaved as documented. Text-only release; blob untouched. ## [0.12.11] — 2026-09-21 ### Added - **The card never said that an Ollama tag does not track this repo.** Ollama resolves a tag once, at pull or create time, and never re-checks afterwards, so a tag made before a `template` / `system` / `params` change keeps serving the old one indefinitely — with no warning, and with `ollama list` showing only the pull date. The two re-pull notes already on the card cover specific stale *blobs*; neither stated the general rule, and neither mentioned the chat template. New **Keeping your tag current** subsection under [Ollama](README.md#ollama) states it, gives a check, and names the refresh for each delivery path. ### Notes - Observed on the companion `FoolDev/Janus-35B-HERETIC` repo, which ships an identical Go `template`: a local tag there served a four-release-old copy for two days. The equivalent gap here would span 0.12.7's tool-chain render and 0.12.8's system-block fix — the last two releases that touched `template` — so a stale tag would emit an out-of-date tool format while every check in this repo passed against the repo's own files. - The check deliberately compares with `$(...)` on both sides rather than piping into `diff` or `md5sum`. This repo's `template` carries no trailing newline and `ollama show` adds one, so the naive form reports a mismatch that is not there — verified in both directions before publishing. - Refreshing is cheap when only the template moved: the weights layer is content-addressed, so an unchanged blob is not re-downloaded. A re-pull of an already-current tag was measured at ~1 s with no change in store size. Text-only release; blob untouched. ## [0.12.10] — 2026-09-19 ### Fixed - **`check.sh`'s pyflakes hint named a command that fails on the maintainer's own host.** It said `pip install pyflakes`, which a PEP 668 distro (Arch, Debian 12+, Fedora) refuses outright — and PEP 668's own error message then steers you to `pipx`, which **cannot** satisfy this check: it runs `python3 -m pyflakes`, so pyflakes has to be importable by the *system* interpreter, and a venv or pipx install leaves the check skipping forever. The hint now says that, and gives both working routes: a `--user --break-system-packages` install, or the distro package. - The card's `scripts/check.sh` row listed the lint steps incompletely (it named `pyflakes` or `shellcheck` but not both) and did not mention that either one is skipped when absent, with the tail reporting the count rather than a green all-passed. ### Notes - With pyflakes present this repo now reports **all 10 checks passed** with nothing skipped, and pyflakes finds no issues in any tracked python file. The `[~]` skip path was still exercised for this release by hiding the user site directory (`PYTHONNOUSERSITE=1`), so the new hint is the text an affected reader actually sees. - One gotcha the hint's `--user` route carries, worth knowing before you pick it: it installs under `~/.local/lib/python3.X/site-packages`, which is scoped to that python version. A distro upgrade to a new python minor silently returns this check to "skipped". The distro package does not have that problem. - Text-only release; blob untouched, sha unchanged. ## [0.12.9] — 2026-09-19 Second tier of the adversarial audit whose top tier shipped in 0.12.8. No template change, no blob change: this release is scripts and card accuracy. ### Fixed (scripts that reported success while doing nothing, or failure while succeeding) - **`cap_ctx.sh` could emit no `num_ctx` override at all and still print `[+] Done. baked at num_ctx=…`.** Its awk only *rewrote* an existing `PARAMETER num_ctx` line, with no `END` fallback — so if the Modelfile ever lost that line, the script produced a Modelfile with no cap and reported success: the exact OOM it exists to prevent. It now appends the override when no line matched, and hard-fails if the patched Modelfile somehow carries none. (`live_check.sh` has had the correct `END` rule since 0.6.4; this is the same shape.) - **`fetch_vision.sh` printed "download failed" after a successful download.** `hf download REPO FILE --local-dir DIR` writes to `DIR/FILE`: only the directory part of `MMPROJ_PATH` was honoured and the basename was discarded, so a custom filename fetched ~900 MB successfully and then failed its own existence check and exited 1 — and the re-run guard probed a path that was never created. It now moves the file into place. `MMPROJ_PATH` is also documented in the usage header for the first time; it was readable only from the source. - **`verify_arch.py` said "no GGUF found" about a file that is right there.** In any clone made without git-lfs the bundle is a ~136-byte pointer; the size filter correctly refused it but the message diagnosed the wrong problem. It now recognises the pointer and names `git lfs pull` / `load_bundle.sh`. Exit code 2 is documented, and so is the `gguf` dependency — previously the first thing a reader hit was a bare `ModuleNotFoundError`. - **`build.sh` and `load_bundle.sh` discovered a missing `gguf` package only after downloading the quant.** Both now preflight their python imports before any download. - **`check_bridge_sync.py` silently dropped any `*_SYSTEM` constant that is not a literal.** `ast.literal_eval` raises on an f-string or a `"".join(...)`, and the bare `except: continue` turned that into a pass — a drifted system prompt written that way exited 0 with `[ ok ] … in sync`. It now prints a `[~]` line naming the constant it could not verify. Deliberately not a hard failure: this script is the pre-commit hook and a non-literal is legal, merely unverifiable. - **`strip_mtp.py` raised a TypeError traceback instead of its own diagnostic** when `{arch}.block_count` was missing (the `general.architecture` lookup two lines above already had that guard), and it **dropped exactly one block regardless of `nextn_predict_layers`** — a model declaring two MTP heads would have been half-stripped and reported as a success. It now refuses. Both paths tested against a stubbed reader. - **`live_check.sh`'s `ollama create` — the one call that writes ~21 GB into a model store — routed only via an ambient `export` 24 lines earlier.** That export going missing is exactly what shipped in 0.6.1–0.6.3 and imported the blob into the user's own store; the 0.6.4 guard fires *after* create, so it can detect but not prevent. The call now carries an inline `OLLAMA_MODELS` / `OLLAMA_HOST` prefix, like the server launch above it. - **`check.sh` promised to ignore vendored directories and did not.** Only `.git` and `__pycache__` were excluded, so a repo-local `.venv` would be fed to shellcheck and `py_compile`, and the `rm -rf` sweep would reach inside it. The exclusions `.gitignore` already anticipates are now real. ### Added (two more guards, both negative-tested) - **The default-tier check was scoped to the wrong slice.** It counted `$effort` assignments inside the `{{ if $.ThinkLevel }}` body only, so an assignment placed *after* that guard — `{{ if not $.ThinkLevel }}{{ $effort = … }}`, say — moved this repo's documented default tier while the check still printed `[ ok ]`. Verified: the old check passes that mutant, the new one fails it. - **Nothing compared the tool-call tag advertised to the model with the tag Ollama actually parses.** The `` preamble is prose to every other assertion here, so the system block could teach `` while the parser scanned for `` and every tool call came back as plain text — 0.9.5's outage reached by a different route. The check now requires the derived tag to appear in the preamble. ### Fixed (card claims that were not true of the shipped files) - **The "turn replay off" recipe told the reader to replace a line no file contains.** It quoted `{{ if (and $.IsThinkSet .Thinking) -}}`; the shipped condition has been `{{ if $.IsThinkSet -}}` since 0.9.5. The replacement side was right; only the "from" side was stale, so anyone following it literally found nothing to change. - **The replay section described pre-0.9.5 behaviour as current**, saying every assistant message *that carries reasoning* renders a block. The condition is `$.IsThinkSet` alone, so every assistant message renders one — an **empty** `\n\n` when the turn did no reasoning. Three other places in the repo already said so. - **There is no `xhigh` think level.** Ollama's levels are `low`/`medium`/`high`/`max`; `xhigh` and `ultra` are clamped to `max` before any template runs, and `api.ThinkValue.IsValid()` accepts only those four. "xhigh" is this repo's name for the *instruction text* the `high`/`max` arm inserts. The card presented it as a level in a table column, which would lead anyone hand-editing the template to write `eq $.ThinkLevel "xhigh"` — a condition that can never fire. - **The template-selection rule was stated without its deciding clause.** Ollama does not simply prefer whichever template advertises more capabilities: a Go template that renders a tool round trip is kept even when the embedded one advertises more, unless the embedded one renders a round trip too (`shouldPreferChatTemplate`, `server/images.go`). The card's conclusion was right; it reasoned from an incomplete rule to get there. - **`ollama_chat.py` pointed at `OLLAMA_NEW_ENGINE=1`**, an environment variable that existed at v0.24 and is gone by v0.33.3. Thinking is separated from content with no flag at all: Ollama defaults `think` to true for any model whose template advertises the capability, as this one's does. - **`llama_cpp_vision.py` justified its chat handler with a false fact.** Qwen 2.5 VL and Qwen 3.x vision do **not** share a projector layout — `qwen2.5vl_merger` and `qwen3vl_merger` are distinct types and the qwen3vl family adds deepstack tensors. The script works because mtmd reads the projector type from the GGUF; the handler only supplies the ChatML wrapper. As written it invited the conclusion that a Qwen 2.5 VL mmproj is substitutable, which it is not. - **The Modelfile quoted a thinking-block condition the template does not carry** (`(and $.IsThinkSet .Thinking)`), which the Go-template guard explicitly forbids — the same stale quote the README carried, on a file the two repos otherwise keep in lockstep. ### Recorded (how Ollama actually hands messages to a template) - **Ollama does NOT pass the message list to the template unmerged** — the sibling's changelog said so, citing `server/prompt.go`, and this repo reasons about the same byte-identical template. Measuring it turned up something worse. `collate` (`template/template.go`, called from `Execute` before the template runs) merges consecutive same-role messages; `server/prompt.go` only truncates. Measured on Ollama 0.33.3 with its own renderer: two consecutive assistant turns arrive as **one** turn with the contents joined by a blank line — and when the second turn carries `tool_calls` and `thinking`, **both are silently dropped**, leaving no `` block at all. The operative conclusion still holds, and is now confirmed from the other direction: `tool` is the exempted role, so consecutive tool results really do reach the template separately, which is why the grouping this template does is necessary. ### Notes - Blob untouched: text-only release, sha unchanged. - Still open from the audit, and deliberately not taken here: pinning the ChatML turn delimiters outside the tool branch in `check_go_template.py` (a real guard-strength gain against a real cost — every legitimate one-token edit to those branches becomes a check failure, and the last four releases were exactly such edits), and extending `verify_arch.py` with `rope.freq_base` plus a YaRN-absence assertion, which can only be validated against the real blob. ## [0.12.8] — 2026-09-18 ### Fixed (the system block no longer ends on a blank line) - **A request with no tools rendered `{system}\n\n<|im_end|>` where Qwen renders `{system}<|im_end|>`.** The `\n\n` after `{{ .System }}` is the correct separator when the `# Tools` block follows it, but with no tools nothing consumed it; the same dangling pair followed the `$effort` line. Both separators are now conditional — `{{ if or .System .Tools }}` after `$effort`, `{{ if .Tools }}` after `.System`. This fired on the **default** path: the Modelfile bakes a `SYSTEM` and Ollama passes it as `.System`, so every request without a `tools` array carried the stray blank line. - Neither new condition names `.ToolCalls`, so Ollama's `parseTag` still lands on the tool-call block — verified after the edit: derived tag ``, capabilities `completion tools thinking` read back with `ollama show`. ### Fixed (correcting 0.12.7's own render claim) - **0.12.7 said "10 of 12 [shapes] now match byte for byte". That was body-only, and the sentence did not say so.** The comparison behind it stripped the system block from both sides and masked the tool-call payload — each a documented deliberate difference, but unstated — and **none of the twelve shapes carried a system message**, which is precisely what hid the defect above. The matrix is now sixteen shapes, four of them system-carrying. The honest numbers, against this repo's own `chat_template.jinja`: | comparison | 0.12.7 | 0.12.8 | |---|---|---| | whole prompt, no-tools shapes | 2 / 7 | **6 / 7** | | message body only (system block excluded) | 14 / 16 | 14 / 16 | The remaining miss in both columns is the one deliberate difference — with the last message an assistant turn, the Go path leaves it open for prefill. With tools in play the whole-prompt comparison never matches by design: the Go template's system block teaches JSON tool calls where the Jinja one teaches Qwen's XML form. ### Fixed (top tier of an adversarial repo audit) - **`scripts/check.sh` rejected every commit after any `make build`.** Check 5 counted `*.gguf` on the filesystem, but `build.sh` defaults its download into the repo root and writes a `*.stripped.gguf` sibling beside it — three files, check fails, and with `check.sh` wired into the pre-commit hook every `git commit` was refused. Both extras are gitignored, so `git status` stayed clean and gave no hint at all. The scan now drops paths git ignores; a file git does **not** ignore still counts, which is the case the check exists for, and the failure message now names what it found. - **`examples/transformers_quickstart.py` could not produce a token.** On transformers v5 — the only line that can load this base — `apply_chat_template` hands back a `BatchEncoding`, so the v4-style `model.generate(inputs, …)` raised `AttributeError` on `.shape`, as did the decode slice. It now passes `return_dict=True` explicitly, calls `generate(**inputs, …)` and slices with `inputs["input_ids"]`, which is correct on both v4 and v5. - **The ROCm build recipe used `-DGGML_HIPBLAS=on`**, which ggml renamed to `GGML_HIP`. CMake reports an unknown `-D` only as a trailing note after a full source build, so an AMD user waits out the build and gets a CPU-only wheel. Fixed in `llama_cpp_quickstart.py` and `llama_cpp_vision.py`. ### Fixed (a mechanism this card repeated in six places does not exist) - **There is no "Ollama Go engine", and no "C++ llama.cpp fallback" distinct from it.** Since [ollama/ollama#16031](https://github.com/ollama/ollama/pull/16031) every GGML model is served by the bundled llama-server subprocess: at v0.33.3 the source tree carries no vendored `llama/llama.cpp/` and no Go model implementations at all — `model/` holds only `parsers/` and `renderers/` (checked 2026-09-18). The card used that imaginary split twice: to explain why the bundled llama.cpp version does not affect text inference, and to explain why mmproj fails. In the Vision table it contradicted itself two lines apart — upstream *has* the arch entry, then the fallback *lacks* it. Corrected in the README, the loader table, `examples/README.md`, `examples/llama_cpp_vision.py` and `scripts/fetch_vision.sh`. - **The Ollama vision verdict is now `❓`, not `❌`.** The failure was observed and that observation stands; the cause given for it was wrong, and nothing has re-tested it since ollama/ollama#14575 closed on 2026-08-04. "The only working vision path right now" is gone. ### Added - **`check_go_template.py` pins the whole system block**, not just the `$effort` emission: the `.System` emission and both conditional separators are now part of the assertion. Deleting `{{ if .System }}{{ .System }}` used to be undetectable — `live_check.sh` compares token deltas that shift equally, `smoke_test.sh` only greps for leaked control tokens, and `check_bridge_sync.py` compares the two copies to each other, so all three stayed green while every request lost its persona. ### Notes - Blob untouched: text-only release, sha unchanged. - Provenance of the last three sections: an adversarial audit of the 0.12.7 tree — nine independent passes, 66 raw findings, **41 surviving** an independent refutation round. What lands here is its top tier. Still outstanding and deliberately not in this release: stale doc claims elsewhere in the card, four more guard gaps in `check_go_template.py`, `cap_ctx.sh` reporting success while emitting no `num_ctx` override, `fetch_vision.sh` reporting failure after a successful download, and defensive gaps in `strip_mtp.py` and `check_bridge_sync.py`. ## [0.12.7] — 2026-09-18 ### Fixed (the Go template renders a tool chain exactly as Qwen's template does) Both remaining whitespace differences between the Go `template` and Qwen's format are gone, and neither fix touches the `range .ToolCalls` body that 0.12.5 broke. Verified by rendering twelve conversation shapes through Ollama's own renderer (`/api/chat` with `"_debug_render_only": true`, 0.33.3, against a 1.2 MB `stories260K.gguf` in an isolated CPU-only daemon) and diffing each against `chat_template.jinja` rendered by jinja2: **10 of 12 now match byte for byte, against 4 of 12 on 0.12.6.** The two that still differ are the one deliberate difference — with the last message an assistant turn, the Go path leaves it open for prefill, with no `<|im_end|>`. - **The blank-line residue 0.12.6 documented is fixed.** An assistant turn with tool calls and no text rendered two blank lines after `` where Qwen renders one: the thinking block ended with a blank line and the `range .ToolCalls` body adds its own leading newline on top. That blank line is now conditional — `{{ if and $.IsThinkSet $blank }}` — with `$blank` false exactly when the turn has tool calls and no text. - **The blank line between an assistant turn's text and its first `` is back**, the alignment 0.12.5 attempted and 0.12.6 reverted. 0.12.6 recorded that it "cannot be had without placing an action exactly where the parser's prefix has to be". **That is wrong, and this release is the counter-example**: only the FIRST tool call needs the extra newline, and whether it is needed depends on `.Content` alone — not on the loop index — so the separator can be emitted *outside* the range, where an action is harmless. The range body is still byte-identical to 0.12.4's. ### Fixed (a second template-derived behaviour, found while fixing the first) Both of these were found by reading Ollama v0.33.3's source and then reproducing them here, not by a render diff — neither is visible in rendered bytes. - **A condition that names `.ToolCalls` *before* the tool-call block breaks tool calling just as thoroughly as an action inside it.** `parseTag` (`tools/template.go`) searches for the FIRST `{{ if }}` whose pipe holds a `.ToolCalls` field anywhere in the template, and reads the first literal text in THAT block; a new `{{ if … .ToolCalls }}` earlier in the file captures the search, yields no text, and the tag falls back to `{`, which is 0.12.5's failure exactly. The obvious way to write this release's fix walks straight into it. The template therefore aliases the field once — `{{ $calls := .ToolCalls }}` — and branches on the variable, which is a VariableNode and never matches that search. - **Ending the thinking block with an `{{ if }}` silently drops the thinking capability.** `InferTags` (`thinking/template.go`) takes the first and last nodes of the list holding `{{ .Thinking }}` as the opening and closing tags, and both must be literal text. The first draft of this fix put the conditional blank line inside that block; Ollama then answered `"does not support thinking"` with HTTP 400 on any request that sets think. The conditional now sits after the block, gated on `$.IsThinkSet` so the no-thinking path renders as before. ### Added - **`check_go_template.py` emulates both of Ollama's AST readers** and asserts what they derive: the tool-call tag must come back ``, the thinking tags `` / ``. Ports of `parseTag` and `InferTags` from v0.33.3, with the Go-template lexing (including `{{-` / `-}}` trimming) and parsing they need. The port returns the known-correct verdict for all three templates whose live behaviour is on record: `` for 0.12.4 (2/2 first-turn calls parsed), `{` for 0.12.5 (0/5), `` for 0.12.6 (3/3). Negative-tested against three mutations — 0.12.5's template, a naive version of this release's fix that names `.ToolCalls` in the condition, and the first draft that ended the thinking block with an `{{ if }}` — which fail the tag check, both checks, and the tags check respectively. - These are the first checks here that read the template's parse tree instead of matching its text. That matters because both failure modes are invisible to a render diff: 0.12.5's rendered bytes were identical to 0.12.4's for every shape tested, so until now only a live tool call against the bundled blob could catch them. `make check` now catches both in milliseconds. ### Notes - **The blob is untouched** — this is a text-only release, sha unchanged, no re-download. - Not covered offline: that Ollama's tool-call *parser* behaves as the derived tag implies. The derived tag and the advertised capabilities (`completion tools thinking`, read back with `ollama show`) are identical to 0.12.6's, which was measured live at 3/3 first-turn calls on the Janus sibling's blob, whose Go template is byte-identical to this one's, so the parser's configuration is unchanged by construction. A `make live-check` on the next Ollama upgrade stays the standing confirmation. - The Go `template` remains byte-identical to the Janus sibling's; both got this change. ## [0.12.6] — 2026-09-18 ### Fixed (0.12.5 broke tool calling — upgrade straight past it) - **0.12.5's tool-call separator silently disabled Ollama's tool-call parsing.** Ollama does not parse tool calls with a fixed grammar: it derives the parser from THIS template, taking the literal text at the head of the `range .ToolCalls` body as the prefix to scan for in the model's output. 0.12.5 put an action there — `{{- if $j }}`, the blank-line separator — so the derived prefix no longer matched and Ollama stopped recognising tool calls at all. The model still emitted a perfectly correct `` block; it arrived as plain text in `content` with `tool_calls` empty. Measured on the real 35B-A3B blob of the Janus sibling, whose Go template is byte-identical to this one: **0/5 first-turn tool calls parsed on 0.12.5, 2/2 on 0.12.4, 3/3 after this fix**, and `scripts/live_check.sh` goes from 16/17 back to **17/17**. - The tool-call block is byte-identical to 0.12.4's again. **The blank line 0.12.5 put between an assistant turn's text and its first `` is gone with it.** That cosmetic alignment with `chat_template.jinja` is not worth breaking tool calls for, and it cannot be had without placing an action exactly where the parser's prefix has to be. **Superseded by 0.12.7: it can — outside the range, keyed on a `$calls` alias — and both blank lines are back.** - **What 0.12.5 got right, and this release keeps:** consecutive tool results still share one `<|im_start|>user` turn, and reasoning still renders as `\n…\n` plus a blank line, including the empty block for a turn that did no reasoning. - **Known cosmetic residue:** an assistant turn that has tool calls and no text renders one extra blank line after ``, because the range supplies its own leading newline where 0.12.5's construct did not. `chat_template.jinja` has no such line. It costs one token and does not affect parsing. **Fixed in 0.12.7.** ### Added - **`check_go_template.py` pins the parser prefix**: the `range .ToolCalls` body must begin with literal text, never an action. This is the assertion that would have caught 0.12.5 offline; it replaces the separator assertion, which pinned the very construct that caused the bug. Worth recording why the existing guards all passed: the rendered BYTES were identical between 0.12.4 and 0.12.5 for every conversation shape tested, so no render diff could see it — the break was in the template's AST, which only a live tool call against a real model exposes. - Also worth recording: `live_check.sh` never sends two consecutive tool results, so it does **not** exercise the tool-response grouping this release keeps. That path is still render-tested only. - This repo's own blob was not live-checked (it is a git-lfs pointer here and would need a 16.5 GB download); the `template` file is byte-identical to Janus's, which was. ## [0.12.5] — 2026-09-18 ### Changed (the Go template renders a tool chain the way Qwen's own template does) Ported identically to the Janus sibling's 0.9.5 — the two repos' Go `template` files stay byte-identical. Verified by rendering the same conversations through this repo's `chat_template.jinja` and through Ollama's own renderer (`/api/chat` with `"_debug_render_only": true`, 0.33.3) against a 1.2 MB `stories260K.gguf` in an isolated CPU-only daemon, and diffing. - **Consecutive tool results now share one `<|im_start|>user` turn**, carrying one `` block each — what `chat_template.jinja` has always done on the llama.cpp path. The Go template used to emit a complete turn per tool result, so a parallel tool call rendered as two user turns on Ollama and one on llama.cpp. - **A blank line separates an assistant turn's text from its first ``**; it was a single newline. - **Replayed reasoning is wrapped as `\n…\n` plus a blank line**, and a turn that did no reasoning renders the empty `\n\n` block, matching `chat_template.jinja` exactly. The condition is `$.IsThinkSet`, not `(and $.IsThinkSet .Thinking)`. - Nine test conversations now render **identically** on the two paths, modulo the two deliberate differences this card documents: the tools preamble and the JSON-vs-XML tool-call body. `ollama show` still reports `tools` and `thinking`. - **Text-only. The blob sha256 is unchanged from 0.12.4** (`24a2d3a2…`) — the Go template is not embedded in the GGUF, so nobody re-downloads. - `scripts/check_go_template.py` gained assertions pinning the grouping, the separator and the new `` wrapper. ### Fixed - **`scripts/heretic_remote.sh` defaulted `MODEL` to this repo**, which is GGUF-only — no `config.json`, no safetensors — so `from_pretrained` could never load it and `SUBMIT=1 ./scripts/heretic_remote.sh` with defaults would claim an a100-large slot and die. It now defaults to the vanilla base `Qwen/Qwen3.8-27B`, the model README "Provenance" names. This never worked: the default was self-referential from the commit that introduced it, not a casualty of a later rename. - **`scripts/build.sh`'s `Requires:` line omitted python3 and the `gguf` package**, although the script runs `strip_mtp.py` unconditionally — so the missing import surfaced only after a multi-GB download had already completed. - **`scripts/check.sh`'s header enumerated 9 checks while 10 are implemented** and the tail already printed "N of 10". The header also never said that a check whose tooling is missing is reported as skipped and never counted as a pass. ### Documentation - **The sibling is Janus-**35B** again** — the README badge and the `CITATION.cff` sibling entry still said "Janus-27B", left over from the 0.8.x dense window. 0.12.3 corrected the body and missed both. - **The "Janus vs Thanatos" section still described the dense twin.** The "Everything else is shared" paragraph listed this model's own shape — 64 layers, hidden 5120, 24 Q / 4 KV heads, the `qwen35` arch stamp — as shared with a model that has been a 40-layer MoE since 0.9.0. It now separates what the two genuinely share from what is this model's alone. - **Janus's comparison figures are no longer "derived rather than measured"** — they have been measured since its 0.9.2. Both sides of the table are now dated and sourced. - **The Modelfile still claimed the Jinja and Go paths disagree on an unset `reasoning_effort`.** 0.12.4 made both default to medium. The README's reasoning-effort prose carried the same stale claim from a llama.cpp run that predates 0.12.4, and is now scoped to what that run actually still shows. - **The Modelfile's hardware block was still Janus's.** Its provenance line claimed the same attention layout (64/16 layers vs 40/10), a 149.62 MiB recurrent state (Janus: 62.81) and a 0.7 GiB weight gap (really 4.37). Replaced with this repo's own 2026-09-18 measurements, including the compute buffer at 8K/32K/64K and the 5.08 tok/s run; the totals move to ~32.3 / ~24.8 GiB. Two README call-outs that still rounded to ~33 / ~25 GiB now agree with the card's own table. - **`make cap-ctx` was described as baking the pulled `hf.co` tag.** It bakes a local `thanatos-27b` tag from the bundled blob and this repo's Modelfile; the `/v1` caveat that motivates it was missing from one of the three copies. - **`chat_template.jinja` has three deliberate changes from upstream, not two** — the file itself and the README's other copy both said three. - **The reasoning field-order credit is past-tensed.** Janus ships no `chat_template.jinja` since its 0.9.0, so "the Janus sibling's order" now says when that was true. ## [0.12.4] — 2026-09-18 ### Changed (the reasoning-effort default is medium on both paths) - **`chat_template.jinja` now resolves an unset or unrecognised `reasoning_effort` to `medium`, not upstream's `xhigh`.** The same request used to answer at a different tier depending on which template rendered it — xhigh through llama.cpp, medium through Ollama, where an unset think level adds no instruction at all. Both paths now default to medium; xhigh is something a client asks for explicitly. `high`, `xhigh`, `max` and `ultra` still map to xhigh, `low`/`minimal` to low, `none` still switches thinking off. The Go `template` needed no change — it already behaved this way, and it is byte-identical to the Janus sibling's. - **The GGUF is restamped** so the embedded copy matches: `strip_mtp.py --chat-template` rewrote the metadata only — 45 KV keys copied, 851 tensors kept, 0 dropped, no re-quantization — and `verify_arch.py` confirms all 15 Architecture claims plus "Embedded chat template == chat_template.jinja (17751 chars, identical)". **The blob has a new sha256** (`24a2d3a21b5a4011d22acc7deb032fe252999dfec08c261409c71f8fa5b8e896`), so an existing puller will fetch it again on the next `ollama pull`. - **Nine new assertions pin the new default** in `check_chat_template.py`: unset, explicit `null`, and `auto` / `""` / `banana` / `5` / `True` all resolve to medium. Nothing pinned it before — the existing tier assertions only covered values a client names explicitly, so the default could have regressed silently. ### Measured (on this repo's own blob, not the sibling's) - **The Hardware section is this model's own measurement.** Earlier releases carried figures measured on Janus while it shared this architecture. Measured 2026-09-18, Ollama 0.33.3 CPU backend, isolated store, at `num_ctx` 8192 / 32768 / 65536 plus a q8_0 pass: weights **15.40 GiB** (`CPU model buffer` 4935.19 MiB + `CPU_REPACK` 10835.16 MiB), recurrent state **149.62 MiB** (constant), KV **65,536 B/token at f16** (512 / 2048 / 4096 MiB) and **34,816 B/token at q8_0** (1088 / 2176 MiB), compute buffer **280.04 / 328.04 / 784.07 MiB**. llama.cpp logs the cache as "16 layers". - The borrowed figures were accurate — 15.4 GiB, 0.15 GiB, 2 GiB and 1.06 GiB per 32K all held — so this is a provenance and precision fix: the compute buffer was a "0.3-0.8 GiB" range and is now three exact values, and the totals tighten to **17.9 GiB at 32768** and **20.3 GiB at 65536** (sums of measured parts), ~32.3 GiB at the 262144 default and ~24.8 GiB with q8_0. - **Throughput re-measured: 5.08 tok/s** aggregate (1,852 tokens / 364,560 ms; 5.32 / 5.10 / 5.07), against 5.01 from the 0.12.0-era run. For scale, the Janus sibling's MoE measured **24.97 tok/s** on the same CPU the same day — 4.9x, and that gap is now measured on both sides rather than inferred from active-parameter counts. ### Added (check harness) - `scripts/check_go_template.py` pins the Go template's reasoning-effort mapping — the xhigh and low arms with their instruction strings, and the medium/unset default on this path. Negative-tested against mutated templates; every mutant fails. It extends check 9 rather than adding one, so the count is unchanged. - The check descriptions in `check.sh` and the README file table now name the effort mapping the guard covers. ## [0.12.3] — 2026-09-18 ### Fixed (the sibling comparison described a Janus that no longer exists) - **Janus went back to the Qwen 3.6 35B-A3B MoE in its 0.9.0**, hours after 0.12.1 recorded it as the dense Qwen 3.8 27B. Every claim here that the two are "the same model on paper", that "neither is a mixture-of-experts", or that Janus offers no capacity argument was false the moment that landed. The Janus vs Thanatos section is rewritten around what actually differs now: dense 27B vs 35B-A3B MoE, 64 layers vs 40, no experts vs 256 (8 routed + 1 shared), ~15.4 GiB resident vs ~19.78 GiB, and — the interesting inversion — 2 GiB per 32K of KV here against 0.625 GiB there, because the MoE has 10 full-attention layers to this model's 16. Janus's figures are marked as derived from its GGUF geometry; this card's remain measured. - Corrected the same assumption everywhere else it had leaked: the intro's "same dense base, differing only in abliteration", the vocab note, the memory-section comparison, the GPU-offload attribution, the duplicate-tool-call note (which was measured when Janus *was* this base, and now says so), the dense-footprint limitation, and the Related models row. - Nothing about **this** model changed. Thanatos has been the dense Qwen 3.8 27B throughout and did not move; only the statements about the sibling were wrong. ## [0.12.2] — 2026-09-18 ### Fixed (`heretic_remote.sh` submitted against the wrong `hf`) - **On Arch, `/usr/bin/hf` is an unrelated hidden-file utility, not the Hugging Face CLI**, and the script ended in a bare `exec hf jobs run …`. On this machine that resolves to a binary with no `jobs` subcommand, so a submit would fail in a way that reads like an auth problem. The script now takes `HF_BIN` (default `hf`), refuses to submit when the resolved binary has no `hf jobs` subcommand, and prints the venv recipe in the error: `python3 -m venv ~/.cache/hf-cli-venv && ~/.cache/hf-cli-venv/bin/pip install -q huggingface_hub`, then `HF_BIN=~/.cache/hf-cli-venv/bin/hf SUBMIT=1 ./scripts/heretic_remote.sh`. The preflight only runs on a real submit, so dry-runs still work anywhere. ## [0.12.1] — 2026-09-18 ### Documentation (parity with the Janus sibling's 0.6.5-0.8.0 work) - **The Ollama-derived claims were pinned to v0.33.3 and v0.34.0.** Ollama has since shipped 0.34.1 (2026-09-14) and 0.34.2 (2026-09-15). Compared in source on 2026-09-17: `openai/openai.go`, `server/prompt.go` and `template/template.go` are byte-identical from v0.33.3 through v0.34.2, so the reasoning-replay field table, the `/v1` reasoning-effort mapping and the template-selection behaviour all still hold. `server/images.go` was refactored but its selection conditions were not: a built-in renderer still displaces both templates only when the model's config names a renderer or parser, which this repo's does not — so **Ollama still runs the Go template**. Nothing was re-tested live; the live figures keep their original attribution. - **Which llama.cpp an Ollama carries** is now stated in Chat template: 0.33.3 and 0.34.0 bundle b10760, 0.34.1 bundles b10864 and 0.34.2 b10969. From 0.34.1 that is a b10763-or-later llama-server, which changes nothing here because text inference runs Ollama's Go engine. - **The loader table's `Ollama 0.24+` floor** was undated and unsourced. Replaced with the three versions actually checked in source, in the card, `examples/README.md` and `examples/llama_cpp_vision.py`. ### Fixed - **Front-matter routed the repo wrongly.** `library_name: transformers` and `pipeline_tag: image-text-to-text` on a repo that ships no `config.json`, tokenizer or safetensors — HF offered a Transformers snippet that cannot resolve. Now `library_name: gguf` and `pipeline_tag: text-generation`. ### Added (check harness, ported from Janus) - `scripts/check.sh` counts skipped checks and reports "N of 10 checks passed, K skipped" instead of a green "all checks passed" over a check that never ran (`pyflakes` is absent on the maintainer's host, so this was happening). - New check 10: the Modelfile's `FROM` must name the bundled GGUF **and** that file must be tracked by git. An untracked bundle looks fine locally and publishes a repo with no weights — which is exactly what it caught on the Janus side. ### Note on the sibling - Janus restored its 35B-A3B MoE in 0.7.0 on 2026-09-18 and moved back to the dense Qwen 3.8 27B in 0.8.0 the same day, so its CHANGELOG shows a round trip. The Janus vs Thanatos section records it; anyone who pulled Janus during that window has the MoE and should re-pull. Thanatos is unaffected — its base has not changed. ## [0.12.0] - 2026-09-14 ### Changed (earlier reasoning is read from `reasoning` and `thinking` too) - **`chat_template.jinja` reads an assistant turn's reasoning from `reasoning`, then `reasoning_content`, then `thinking`,** the Janus sibling's order; upstream's template read only `reasoning_content`. A non-string value is stringified, and an explicit null falls through to the next field. This is the template's third change marked `CHANGED FROM UPSTREAM`. - **Why:** loaders that hand the template the client's messages unchanged pass whichever field the client used. KoboldCpp does (its `format_jinja` in `koboldcpp.py`, `concedo` `4ac5721`, only adjusts `content` and `tool_calls`), and current vLLM / OpenRouter-style clients send `reasoning`. Rendering both templates with KoboldCpp's own Jinja setup, a trace sent as `reasoning` or `thinking` became an empty `` block under the old template and is replayed under the new one. - **Not affected:** llama-server reads only `reasoning_content` from `/v1` messages before the template runs, so its behaviour is unchanged (master `f3a184b`, b10760 and b10519: `reasoning_content` replayed, a `reasoning` field ignored, as before). LM Studio 0.4.24 normalises `reasoning` to `reasoning_content` and drops `thinking` first, so the old and new templates rendered the same there (tested on its CPU runtime with a 260K-parameter model). Ollama runs the Go template. - **README:** the replay table gains LM Studio and KoboldCpp rows and a note on the field order; the file table and the list of template changes follow. The Reasoning effort section notes that the unset / unrecognised default is xhigh here and medium on Janus. - **The bundled GGUF is restamped** with the new template - `tokenizer.chat_template` only; all 851 tensors and every other metadata key compared byte-identical: sha256 `534fd0a4af4d26e3f6e5c3db5190d327138bda3802f11519b277c04422709158` (was `1e3249a2dd6512dff463e44a07c7ad165d833f28a2015af98e35f2013ef0502f`). An Ollama user re-pulling `hf.co/FoolDev/Thanatos-27B-HERETIC` downloads the ~17 GB blob again. - **Verified, CPU only.** On llama-server master `f3a184b`, b10760 and b10519 with `--chat-template-file`, a string-argument tool replay rendered XML and `reasoning_effort: "max"` and a tools request both returned HTTP 200. On master with the restamped blob, a real first turn at `reasoning_effort: "low"` thought briefly and returned one parsed `get_weather` call, and the replay carrying that `reasoning_content` returned HTTP 200 with the right answer. - **`scripts/live_check.sh` on Ollama 0.33.3 passed all 17 checks** on the restamped blob, with `ollama list` on the default server unchanged: Ollama still selects the Go template, a first-turn call comes back once, a replayed string-argument tool result is answered, and reasoning replay and the effort tiers are unchanged (232 / 187 and 375 / 330 prompt tokens, as in 0.11.0). ### Fixed (live checks talked to the wrong Ollama) - **`scripts/live_check.sh` in 0.11.1 and 0.11.2 did not use its isolated server.** The CPU-only guard added in 0.11.1 replaced the line `export OLLAMA_HOST="127.0.0.1:${PORT}"` instead of keeping it, so `ollama create` imported the GGUF into whatever Ollama answered on the default `127.0.0.1:11434` - normally your own store - and every check then queried the isolated server, which had no model, and failed with HTTP 404. No model was loaded or run by those checks, but a run could leave a `live-check-thanatos-27b-heretic` model in your own store: remove it with `ollama rm live-check-thanatos-27b-heretic`. The export is back where it was before 0.11.1, and the script now also exits before any check if the imported model is not on the isolated server. Found when this release's first live run failed 17 of 17 with 404s; the rerun above is with the fix. ### Changed (checks) - `scripts/check_chat_template.py` asserts that reasoning in `reasoning` and in `thinking` is replayed, that `reasoning` wins over `reasoning_content`, and that an explicit null `reasoning` falls through: 52 assertions, up from 48. The first three fail against the 0.11.2 template. ## [0.11.2] - 2026-09-14 ### Documentation (LM Studio tested; q8_0 cache command) - **The LM Studio app handles string tool-call arguments.** Tested through LM Studio 0.4.24's `/v1/chat/completions` on its CPU runtime (avx2 2.37.0), with a 260K-parameter model carrying this repo's template: a replayed call whose `arguments` was the string `{"city": "Paris"}` returned HTTP 200, and LM Studio's log shows it rendered as ``, so the app parses the arguments before its own Jinja engine renders. `arguments` that are not valid JSON return HTTP 500 before the template runs, with or without `tool_call_format: "auto"`. The Tool / function calling section replaces 0.11.1's "unconfirmed" note with this; KoboldCpp, Jan and LM Studio are now all covered. - **The context note shows how to halve the KV cache:** `OLLAMA_KV_CACHE_TYPE=q8_0 OLLAMA_FLASH_ATTENTION=1 ollama serve`, or the same as `Environment=` lines via `sudo systemctl edit ollama`. That is what brings the 262144 default within reach of a 32 GB host. ## [0.11.1] - 2026-09-14 ### Fixed (memory figures measured, not extrapolated) - **The context-memory numbers were wrong.** The `Modelfile` notes and README said "~2 GB per 32K (q8_0)", "~22 GB weights + compute" and "~38 GB at 262144" (the last added in 0.11.0). Measured on Ollama 0.33.3's CPU backend from llama.cpp's own allocation lines, at `num_ctx` 8192, 32768 and 65536: Measured on the Janus sibling, which has the same attention layout; Thanatos's weights are 0.7 GiB smaller, and its totals below account for that. - The KV cache covers the 16 full-attention layers (4 KV heads x 256 dims) and is exactly 2 GiB per 32K tokens at **f16**, Ollama's default, and 1.06 GiB per 32K at q8_0. The old figure was the f16 number labelled q8_0. - Weights take ~15.4 GiB; the 48 linear-attention layers add a fixed 0.15 GiB; the compute buffer was 0.3-0.8 GiB with flash attention, which Ollama's default `auto` enabled. Forcing flash attention off grows it to ~25 GiB at 262144 (llama.cpp master's `llama-fit-params` projection). - Totals at the 262144 default: **~33 GiB** with the f16 cache, **~25 GiB** with `OLLAMA_KV_CACHE_TYPE=q8_0` and flash attention; at 65536: ~20 GiB f16, ~18.5 GiB q8_0. Figures at 262144 and above add the fit-params compute-buffer projection (~1-1.5 GiB), which was ~0.35 GiB above what Ollama allocated at 65536; the default was not load-tested because the test host has 27 GB. - So a 32 GB host can run the default with the q8_0 cache (tight), which the README and `Modelfile` hardware rows now say, alongside a `num_ctx` / cache-type table. ### Fixed (live checks refuse to use a GPU) - **`scripts/live_check.sh` now runs Ollama on CPU only.** It starts the isolated server with `OLLAMA_LLM_LIBRARY=cpu`, `OLLAMA_VULKAN=0` and the CUDA / ROCm / Vulkan device lists emptied, waits for Ollama's "inference compute" log line, and exits before importing if any device is not `library=cpu`. On a host whose iGPU also drives the desktop, GPU compute had previously taken the session down; until now the script relied on the caller's environment. Tested against the tiny test model (it reported "CPU only: 1 CPU device(s)") and against a log with a Vulkan device line (refused). ### Documentation (other loaders; a correction) - **Tool / function calling now covers KoboldCpp, Jan and the LM Studio app,** checked 2026-09-14. KoboldCpp v1.120 with `--jinja --jinja_tools` parses JSON-string `arguments` with `json.loads` before rendering; with this template a string-argument replay returned HTTP 200 and rendered XML (tested). Jan's v0.8.4 and main branch run llama.cpp's own server code (janhq/llama.cpp b9967, ggml-org b10809), which converts string arguments (from source). The LM Studio app renders templates with its own JS engine; its bug tracker suggests arguments reach templates as mappings, unconfirmed here. The README gives the workaround if one raises. - **Correction to 0.7.0.** Its tool-calling entry, and the matching comments in `scripts/build.sh` and `scripts/strip_mtp.py` until 0.10.0, said upstream's XML default makes llama-server return HTTP 500 on the second turn of every tool call. That was reproduced by rendering the template directly, not through llama-server, which converts string arguments into mappings before rendering; the raise only reaches loaders that pass the raw string through. The entry is left as written. - **Tool calls with thinking on** also work on the embedded template: on llama-server master `97e4ca7` with default reasoning effort, the Janus blob thought briefly, returned one parsed `get_weather` call, and answered the string-argument replay with HTTP 200. ## [0.11.0] - 2026-09-14 ### Changed (Ollama loads the full native context by default) - **The default `num_ctx` is 262144, up from 65536,** in the `Modelfile` and the HF bridge `params` file, so `ollama run hf.co/FoolDev/Thanatos-27B-HERETIC` and a local `ollama create` both load the full 262,144-token native window. The GGUF already declares that window (`context_length` 262144) and is unchanged; llama.cpp and LM Studio pick their own context size, so only Ollama is affected. - **It needs more memory.** Per this repo's ~2 GB per 32K estimate with a q8_0 KV cache, the KV cache grows from ~4 GB to ~16 GB and the total from ~26 GB to ~38 GB; an f16 cache roughly doubles the KV. A 32 GB host no longer fits the default: lower `num_ctx` per session (`/set parameter num_ctx 65536`), pass `options.num_ctx` on `/api/chat`, or bake a smaller tag with `make cap-ctx` for OpenAI `/v1` clients, which cannot override it. - **Docs follow:** the `Modelfile` hardware notes and working configurations, the README context and memory notes and the hardware table, and the `scripts/cap_ctx.sh` comment now describe the 262144 default. The loader and hardware tables' RTX 5090 and Mac Studio rows now say they need a lower `num_ctx` at 32 GB. - **Checked:** `scripts/check.sh` passes, including the `Modelfile` / bridge-file sync check, and an isolated Ollama 0.33.3 `ollama create` from this `Modelfile` reports `num_ctx 262144`. Not load-tested at the new default: that needs ~38 GB and the test host has 27 GB. ## [0.10.0] - 2026-09-14 ### Changed (llama.cpp tool calls default to XML again) - **`chat_template.jinja` defaults `tool_call_format` to `xml`,** the only form upstream's template has; 0.7.0 to 0.9.0 used `auto`. llama-server recognises this template as Qwen3-Coder-style (it contains ``, ``, that accept only `` - per llama.cpp's `common/chat.cpp` and `common/parsers/qwen3-coder.cpp` at master `97e4ca7`, with the same parser in b10760 and b10519. A prompt teaching the JSON form would get replies that parser cannot read, so pinning XML keeps prompt and parser in agreement. `auto` and `json` stay available as opt-ins through `--chat-template-kwargs` or a request's `chat_template_kwargs`, for loaders that hand the template JSON-string arguments. On llama-server `auto` renders XML anyway, and `json` would break parsing. - **The second-turn 500 that motivated `auto` does not happen on llama-server.** The template's capability probe shows it reads object arguments, so llama-server decodes a client's JSON-string `function.arguments` into a mapping before rendering. Tested on 2026-09-14 with a 260K-parameter test model and `--chat-template-file`, replaying a call whose `arguments` was the string `{"city": "Paris"}`: `/apply-template` and `/v1/chat/completions` returned HTTP 200 with the call rendered as `` on master `97e4ca7`, b10760 (the llama-server Ollama 0.33.3 bundles) and b10519 (LM Studio's avx2 CPU runtime 2.30.0), under `xml` and under 0.9.0's `auto` alike. A per-request `tool_call_format: "auto"` and `reasoning_effort: "max"` also returned 200 on all three. A loader that passes the raw string still gets a raise under `xml`, with a message that names the `auto` opt-in. - **The template keeps two marked changes** (the `tool_call_format` setting, `reasoning_effort`); its comments say why the default is `xml`. - **Ollama is unaffected.** It runs the Go `template` / `Modelfile` TEMPLATE, which keeps JSON tool calls, and never the GGUF's embedded template. - **README:** "Tool / function calling" now explains the parser constraint, the argument decoding and the opt-in; the loader table, the tool-calling summary, the file table and the large-arguments limitation follow. `scripts/check.sh`, `scripts/build.sh` and `scripts/strip_mtp.py` no longer say the upstream template returns a 500 on the second tool-call turn; they name the raise it does have. - **The bundled GGUF is restamped** with the new template - `tokenizer.chat_template` only, tensors identical: sha256 `1e3249a2dd6512dff463e44a07c7ad165d833f28a2015af98e35f2013ef0502f` (was `fb3796d4f5081e47da331771c0a5d7467123a03950762465c1488cf1eeff85b6`). An Ollama user re-pulling `hf.co/FoolDev/Thanatos-27B-HERETIC` downloads the ~17 GB blob again. - **Verified on the restamped blob, CPU only.** Tensors and every metadata key but `tokenizer.chat_template` are byte-identical to the 0.9.0 blob (all 851 tensors compared), and `scripts/verify_arch.py` passes. On llama-server master `97e4ca7` with the embedded template and default flags, a real first turn returned one parsed `get_weather` call, and the replay of that call with its string `arguments` plus a tool result returned HTTP 200 with the right answer (both with `reasoning_effort: "none"`). `scripts/live_check.sh` on Ollama 0.33.3 passed all 17 checks: Ollama still selects the Go template, a first-turn call comes back once, a replayed string-argument tool result is answered, and reasoning replay and the effort tiers are unchanged (232 / 187 and 375 / 330 prompt tokens, as in 0.9.0). ### Changed (checks follow the default) - **`scripts/check_chat_template.py` sections 1-3** now assert that the default renders the XML form and the XML instructions, byte-identical to `tool_call_format='xml'`, and still refuses unconverted string arguments; that `auto` renders string arguments as JSON with matching instructions; and that every format value renders mapping arguments: 48 assertions, up from 45. ## [0.9.0] - 2026-09-13 — `5ce0b54` ### Changed (earlier turns' reasoning is replayed again) - **Reasoning from earlier assistant turns is back in the prompt, on both runtimes.** 0.7.0 stopped replaying reasoning from turns before the latest user message; this release reverses that. A multi-turn conversation now costs more prompt tokens: every earlier trace the client sends back stays in the prompt, and prefill grows with each one - the README's measurement is 181 prompt tokens without and 464 with, on a 3-message conversation whose first assistant turn carried a ~240-word trace. On a CPU-only host prefill is the slow part. - Ollama - the thinking block in the `Modelfile` TEMPLATE and the bridge `template` now opens with `{{ if (and $.IsThinkSet .Thinking) -}}` instead of Qwen's stock `{{ if (and $.IsThinkSet (and .Thinking (or $last (gt $i $lastUserIdx)))) -}}`, so every assistant message that carries reasoning renders `…`. The `$lastUserIdx` loop stays, for anyone who edits the condition back. Ollama never passes reasoning to the GGUF's embedded template, so on Ollama only the Go template matters. - llama.cpp - `chat_template.jinja` restores heretic-ara's line verbatim: `{%- if preserve_thinking is undefined or preserve_thinking is true or loop.index0 > ns.last_query_index %}`. Earlier turns' reasoning is replayed by default, as upstream does, including upstream's empty `\n\n` block for an earlier assistant turn without reasoning; the template reads only `preserve_thinking`, which llama-server derives from `preserve_reasoning` and `--no-reasoning-preserve`. The template now has two marked changes (`tool_call_format`, `reasoning_effort`). - **The client has to send the reasoning back.** Ollama `/api/chat` reads each assistant message's `thinking` field; Ollama `/v1/chat/completions` reads only `reasoning` (it silently drops `reasoning_content` and `thinking`, and an assistant message whose content is an array of parts keeps its reasoning only if it also has `tool_calls`); llama-server's `/v1` reads only `reasoning_content`. From the servers' source (Ollama v0.33.3 and v0.34.0); the `/api/chat` and `/v1` fields were then checked live (below). - **Turning it off:** on llama-server, `--no-reasoning-preserve`, or `preserve_thinking: false` or `preserve_reasoning: false` in a request's `chat_template_kwargs`; on Ollama, change the condition back to Qwen's stock one (do not delete the block). A live tool-call chain keeps its reasoning either way. The README section **Thinking is not replayed across turns** is now **Thinking is replayed across turns**, with the fields each endpoint reads and both ways to turn it off; the `Modelfile` comment, the file table and the list of template changes follow. - **Tested on llama.cpp** with a 260K-parameter test model and `--chat-template-file` (the template, not the model, decides the prompt), via `/apply-template` and `/v1/chat/completions`, on master `4a89937` (b10941), b10760 (the llama-server Ollama 0.33.3 bundles) and b10519 (LM Studio's avx2 CPU runtime 2.30.0): with default flags earlier reasoning was replayed; with `--no-reasoning-preserve`, or per request `preserve_thinking: false` or `preserve_reasoning: false`, it was not; `preserve_thinking: true` replayed it even against `--no-reasoning-preserve`; a live tool-call chain kept its reasoning in every mode; no request returned an error. Master (b10763 and later) passes `preserve_reasoning=true` unless started with `--no-reasoning-preserve` and logs "chat template supports preserving reasoning, it is enabled by default"; b10760 and b10519 pass nothing, log "consider enabling it via --reasoning-preserve", and still replay, because the template's own default is on. - **The bundled GGUF is restamped** with the new template - `tokenizer.chat_template` only, tensors identical: sha256 `fb3796d4f5081e47da331771c0a5d7467123a03950762465c1488cf1eeff85b6` (was `c9895b2acc3defb3fd2def312e36915c11e2565f101584e12bc6c6a537262415`). An Ollama user re-pulling `hf.co/FoolDev/Thanatos-27B-HERETIC` downloads the ~17 GB blob again. - **Verified on the restamped blob, CPU only.** `scripts/live_check.sh` on Ollama 0.33.3 passed all 17 checks: Ollama still selects the Go template, an earlier turn's reasoning is replayed (232 prompt tokens with it, 187 without) and a live tool chain keeps its own (375 / 330). Through Ollama's `/v1` (measured with the Janus sibling's blob; the Go template is byte-identical), an earlier turn carrying `reasoning` counted 215 prompt tokens and one carrying `reasoning_content` 187, the same as none. llama-server master `4a89937` with the embedded template and default flags rendered the 3-message conversation in 111 tokens with the earlier turn's `reasoning_content`, 85 with no reasoning (the empty think block) and 85 with a `reasoning` field instead (dropped); `--no-reasoning-preserve` or either `false` kwarg gave 81, and `preserve_thinking: true` against the flag 111. A real turn after replayed reasoning and a real tool-result turn with string arguments both returned HTTP 200 with the right answer. ### Fixed (template comments deferred from 0.8.0) - **The comment fixes listed under 0.8.0's "Known (deferred to the next GGUF rebuild)" are done**, riding on this release's restamp, so that entry is resolved: the `tool_call_format` block in `chat_template.jinja` is now marked `CHANGED FROM UPSTREAM`, as the header says every change is, and the old no-replay comment, with its claim that llama-server enables `preserve_thinking` by default, is gone along with the change it described. ### Changed (checks expect replay) - **`scripts/check_chat_template.py` section 7** now asserts that earlier reasoning is replayed by default and with `preserve_thinking=True`, dropped with `preserve_thinking=False`, that a conflicting `preserve_reasoning=True` with `preserve_thinking=False` does not raise, and that a live tool-call chain keeps its reasoning with `preserve_thinking=False`: 45 assertions, up from 44. - **`scripts/check_go_template.py` (check 9)** now requires `(and $.IsThinkSet .Thinking)`; Qwen's stock condition fails it. - **`scripts/live_check.sh` check 4** now expects an earlier turn's reasoning to be replayed: more prompt tokens with it than without. Still 17 checks. ### Documentation (upstream fix and report linked) - **The Ollama fix is proposed upstream** as [ollama/ollama#18391](https://github.com/ollama/ollama/pull/18391): it adds the two missing `String()` methods, so `{{ .Function }}` renders JSON again once merged and released. This repo's `{{ json .Function }}` renders the same either way. - **The Vulkan iGPU memory trap is reported upstream** on [ggml-org/llama.cpp#27360](https://github.com/ggml-org/llama.cpp/issues/27360#issuecomment-5637244544), with the mechanism and the `--no-host` measurements from the README's GPU notes. Both READMEs link the two. ## [0.8.1] - 2026-09-11 — `43af6bc` ### Documentation (upstream report) - **The Ollama bug behind 0.8.0's tool-definition fix is reported upstream** as [ollama/ollama#14601](https://github.com/ollama/ollama/issues/14601), with this repo's findings posted there ([comment](https://github.com/ollama/ollama/issues/14601#issuecomment-5636865937)): the cause (commit e51dead6, first in v0.14.0), a model-free repro on current main, the one-file fix, and that open PR #18318 does not reach it. Ollama's own library Qwen models hit it too. If Ollama adds the missing `String()` methods, `{{ json .Function }}` keeps rendering the same JSON, so nothing here needs to change. The README's Ollama tool-calling section now says why the template uses it. ### Documentation (GPU offload on the Strix Halo iGPU measured) - **The card now explains why `num_gpu` alone is not safe on a Vulkan integrated GPU, and what partial offload buys.** On this iGPU llama.cpp (the build Ollama 0.33.3 bundles included) turns mmap off and places every non-offloaded layer in GPU-reachable host memory from the same pool; a llama-server run filled the pool and crashed the desktop. `LLAMA_ARG_NO_HOST=1` keeps those layers in RAM (verified: no Vulkan_Host model buffer). Measured on the Janus sibling (same architecture) with a private Ollama 0.33.3 + `ollama-vulkan` (flash attention, q8_0 KV, `num_ctx` 4096): 4.8 / 4.9 / 5.3 / 5.7 tok/s at `num_gpu` 4 / 8 / 16 / 24, against about 5.0 on CPU; the 32-layer step ran the 27 GiB host short of RAM (GTT is system RAM) and was stopped. ### Documentation (LM Studio tested; loader claims narrowed) - **LM Studio's CPU runtime passed the same 25 checks as standalone llama-server.** Its `llama-server` binary (llama.cpp b10519, the engine LM Studio drives) was run directly with `--device none` on both published blobs: one call per first-turn tool request (three prompts, with and without `parallel_tool_calls`), a single streamed index, the string-argument replay answered, every `reasoning_effort` value accepted, the effort tiers rendered, earlier turns' reasoning not replayed and a live tool chain's kept. - **The LM Studio app itself could not be kept off the GPU, so it stays untested.** In two guarded attempts (snapshot of every LM Studio file, CPU runtime selected, Vulkan hidden, `--disable-gpu`), LM Studio 0.4.24 loaded the model with a newer CPU runtime (avx2 2.34.0) than the one selected, and that runtime's process opened the GPU; the test's guard stopped it before any real GPU allocation, and every LM Studio file was restored byte for byte. LM Studio's own API layer (per its source it maps `reasoning_effort` only to thinking on or off) remains untested. - **The card no longer promises tool calls in loaders nobody tested.** It said LM Studio, Jan, KoboldCpp and llama-cpp-python get working tool calls with no configuration. Now it says llama-server and LM Studio's runtime do (both tested); llama-cpp-python renders the tools but, per its source, does not parse the model's `` output; Jan and KoboldCpp are untested. - `make help` now describes what `make check` runs (lint, bridge sync, the chat-template render checks and the Go-template guard). ## [0.8.0] - 2026-09-11 — `3b0a3f8` ### Removed - **`scripts/heal_hf_pull.sh` and `make heal-hf`.** They made a qwen36-stamped pull load in place, but every such blob is the May 2026 build on vanilla `Qwen/Qwen3.6-27B`, so a healed tag ran the wrong model; since 0.7.0 the docs have said to pull again instead. `scripts/rename_arch.py` and the qwen36 branch of `make load-bundle` stay. Older entries below still mention heal-hf as history. ### Fixed - **Tool definitions reached the model as Go struct syntax, not JSON, on every Ollama since 0.14.0.** The Go template printed each tool with `{{ .Function }}`. Ollama 0.14.0 (#13525) made `.Function` a struct type with no `String()` method, so Go's template engine printed `{get_weather Get current weather for a city {object [city] {...}}}` inside ``, where the model expects a JSON schema. Tool calls still worked in every test, because the tool and parameter names survive, but the model never saw a well-formed schema. The template now uses `{{ json .Function }}`, which renders `{"name":"get_weather","description":..., "parameters":{...}}`. Established by rendering with Ollama's own template package at v0.33.3; the struct is unchanged in 0.34.0 and on main, and the `json` function dates from 0.2.6, so the fix also holds on older builds. check 9 now fails on a bare `{{ .Function }}`. Only the `template` file changes, not the GGUF, so a re-pull downloads a few kilobytes. Re-verified live on Ollama 0.33.3 with `scripts/live_check.sh`: 17 of 17 on both models, one tool call each; with a well-formed schema, a live tool chain's prompt is 6 tokens longer (375 prompt tokens with its reasoning, 369 before). - **The provenance note misattributed the `Teacher:` field and the `datasets:` list to the upstream base.** Neither base's card lists datasets or names a teacher (Hub API `cardData.datasets` is empty for both, and neither base README mentions a teacher, distillation or Crownelius). The note now says they are carried over from earlier revisions of this card, unverified, and that `Crownelius/Opus-4.7-Reasoning` is not publicly reachable. The 0.7.0 entry's explanation of that 401 ("gated") is corrected: the Hub returns the same 401 for a private or missing repo, while a gated repo answers 200. ### Documentation - **Verified on Ollama 0.34.0** (CachyOS `0.34.0-1.1`, already on some CachyOS mirrors though not yet the official ones) with `scripts/live_check.sh` in an isolated daemon on CPU: 17 of 17 checks on both models, with the same numbers as on 0.33.3 - the Go template selected with thinking detected, one tool call at `high`, prior-turn reasoning dropped (187 = 187 prompt tokens), a live tool chain's kept (369 > 324), and the effort lines (159 unset and medium, 185 low, 197 high and max). Its bundled llama-server is still b10760. The README's and Modelfile's "verified on 0.33.3" notes now name both versions. ### Known (deferred to the next GGUF rebuild) - **Three comments inside `chat_template.jinja` are inaccurate:** the header says each of the three changes is marked `CHANGED FROM UPSTREAM`, but the `tool_call_format` default isn't; and the no-replay comment says llama-server enables `preserve_thinking` by default, which only recent builds do. The template is stamped into the GGUF, so a comment fix means a new blob and a full ~17 GB re-download for every Ollama user. They will go in with the next functional template change. ## [0.7.0] - 2026-09-11 — `0326c41` ### Changed (reasoning effort now reaches the prompt on Ollama) - **The Go template adds the reasoning-effort line.** Ollama's default path runs the Go template, which until now added no effort instruction for any value (only `none` changed the prompt, by closing the think block). It now reads Ollama's think level (`$.ThinkLevel`) and adds the line `chat_template.jinja` adds: `high`, `xhigh`, `max` and `ultra` (which Ollama passes on as `high` or `max`) get the xhigh line; `low` and `minimal` (passed as `low`) the low line; `medium` and an unset value (both passed as `medium`) none. Unlike `chat_template.jinja`, whose default is xhigh, an unset value adds no line: Ollama sends it as `medium`, the same as an explicit `medium`. The mapping is from Ollama's v0.33.3 source (`thinkFromReasoningEffort`, `ThinkValue`). An outer `if $.ThinkLevel` keeps the template rendering on Ollama builds that predate the field, and the block sits before the system prompt, outside `range .Messages`, so it leaves the `.Thinking` detection that check 9 guards alone. - **Verified live on Ollama 0.33.3** (`scripts/live_check.sh`: isolated daemon, CPU) against the restamped blob: Ollama still selects the Go template with thinking detected; on a one-line chat `high` and `max` each added the xhigh line (197 prompt tokens against 159), `low` the low line (185), and `medium` and an unset value nothing (159 and 159); a first-turn tool call at `high` came back once; all eight values returned 200, and `none` switched thinking off. ### Fixed (llama-server replayed earlier reasoning anyway) - **Recent llama-server builds switch reasoning replay on by default** for any chat template that honours `preserve_thinking`: they probe the template at load time and, unless started with `--no-reasoning-preserve`, set `preserve_thinking=true` on every request (llama.cpp `common/arg.cpp:958-960`, `common/jinja/caps.cpp`). b10760, the build Ollama 0.33.3 bundles, does not; it only logs a suggestion to enable it. This template honoured the variable as an opt-in, so the "not replayed across turns" default never applied on a current stock llama-server. Tested on llama.cpp master `a2878d3` (CPU) against the published blob: an earlier turn's reasoning was in the rendered prompt by default, and absent with `--no-reasoning-preserve`. - **The template now ignores `preserve_thinking`.** Earlier turns' reasoning is never replayed; the turn in progress, a tool-call chain included, keeps its own. `--reasoning-preserve` has no effect on this template any more; the README gives the one-line edit that restores replay. `check_chat_template.py` now asserts the variable is ignored. - **Re-tested on llama.cpp master `a2878d3` with the restamped blob and default flags:** the earlier turn's reasoning is no longer in the rendered prompt, a live tool chain's still is, and the llama-server warning about preserving reasoning is gone. 25 of 25 checks pass. - **The bundled GGUF is restamped** with the new template - `tokenizer.chat_template` only, tensors unchanged: sha256 `c9895b2acc3defb3fd2def312e36915c11e2565f101584e12bc6c6a537262415` (was `357b0d0af80b60b810a7c6cbbb8044a406ad1f740de39e6167feb4233e4df149`). ### Documentation (standalone llama.cpp tested) - **Standalone llama-server, llama.cpp master `a2878d3` on CPU, the published blobs of both models: 25 of 25 checks each.** Six first-turn tool calls at temperature 0 (three prompts, with and without `parallel_tool_calls`) each came back exactly once, with clean `content` and `finish_reason: tool_calls`; a streamed call used a single index; a replayed tool result with JSON-string arguments was answered; all eight `reasoning_effort` values Ollama accepts, plus `"HIGH"` and an unknown word, returned 200, `none` with no reasoning; and the rendered prompt carried the tiers in the README table. The README's "untested on llama.cpp" notes are replaced with these results, and the duplicate-call limitation now says it did not reproduce there. Nor did it under b10760, the llama.cpp build Ollama 0.33.3 bundles and the one the duplicate came through: 25 of 25 on the published Janus blob, six first-turn calls and a streamed run each returning one call. - **Throughput measured on the current blob, CPU only: 5.01 tok/s** at Q4_K_M (`make bench`, Ollama 0.33.3 CPU backend on a Ryzen AI Max+ 395, flash attention, q8_0 KV). The CPU-only row said ~1-3 tok/s. The Vulkan figures (~12.3 at Q3_K_S, ~9.3 at Q4_K_M) are relabelled: the README said they were taken on the heretic-v2 blob and carry over, but the Modelfile dates them to 2026-05-19, on the Qwen 3.6 27B builds before both base swaps, and they have not been re-measured. ### Added (checks for what render tests miss) - **`check.sh` check 9, `scripts/check_go_template.py`.** It fails if the Go `template` loses the `.Thinking` reference inside `range .Messages` that Ollama infers thinking support from, or if Qwen's stock `$lastUserIdx` condition is narrowed - the two regressions below, each of which passed every existing check. It also asserts the `$lastUserIdx` loop and the tool round trip Ollama credits (`.Tools`, `.ToolCalls`, `eq .Role "tool"`, ``). Negative-tested: it fails on the deleted block and on the `$last`-only condition. - **`scripts/live_check.sh` (`make live-check`).** It starts an isolated Ollama (its own port and model store), imports the GGUF with this repo's `Modelfile`, and checks what only a live server shows: which template Ollama selects, that a first-turn tool call comes back exactly once, that a replayed tool result with JSON-string arguments gets answered, that a prior turn's reasoning is dropped while a live tool chain's is kept (prompt tokens on a fresh load), and every `reasoning_effort` value Ollama accepts. It needs the real GGUF, RAM to load it and several minutes, so it is not part of `check.sh`. Run against the restamped blob before this release: 17 of 17 checks passed. ### Fixed (`heal-hf` is no longer the advice for old pulls) - **The README told anyone with a qwen36-stamped pull to run `make heal-hf`.** That makes the blob load, but every qwen36-stamped blob dates from 2026-05-19/20, before both base swaps (the bundled blob moved to heretic-v2 on 2026-07-24 and to heretic-ara on 2026-08-25; a docs-only heretic-v2 swap on 2026-05-23 was reverted). A healed tag still runs the May build on vanilla `Qwen/Qwen3.6-27B`, with the template and params it was pulled with; the heal rewrites only the model layer. The README, `examples/README.md`, the Makefile help and `heal_hf_pull.sh` (header and closing message) now say to pull `hf.co/FoolDev/Thanatos-27B-HERETIC` again instead. The script stays for anyone who wants the old build to load. ### Fixed (Ollama runs the Go template again) - **Disabling preserve-thinking had silently switched Ollama to a different template.** Deleting the `{{ if (and $.IsThinkSet .Thinking) }}` block from the Go `template` also deleted the only thing Ollama uses to infer that template's "thinking" capability: a `.Thinking` field inside `range .Messages`, wrapped in ``/``. Ollama 0.33.3 (what this was verified on) compares that capability list with the GGUF's embedded Jinja template at load time and prefers the Jinja template when it advertises more, keeping the Go template on a tie; with thinking gone, the Jinja template won. Established from Ollama's source at v0.33.3 (`shouldPreferChatTemplate`, `thinking.InferTags`) and from the service log on the sibling Janus (same Go template; this model's switch was not logged), where one unchanged 0.33.3 process logged `selected=go_template go_template="[completion tools thinking]"` before the change and `selected=gguf_chat_template go_template="[completion tools]"` after it. - **That switch is what exposed the `reasoning_effort` 500s** fixed below. It also exposed a duplicate-call problem: on the embedded-template path a single tool call came back twice (seen once, on Janus). A separate raw generation showed the model drafting the call inside `` before emitting it once after ``, so the parser most likely matched the draft - inferred, not confirmed; not tested at the time on Thanatos, standalone llama.cpp or LM Studio (on standalone llama.cpp it later did not reproduce; see "Documentation (standalone llama.cpp tested)"). And, per Ollama's v0.33.3 source (not tested live), a setup forcing the Go template (`OLLAMA_GO_TEMPLATE=1`) would have lost thinking support and rejected thinking requests with HTTP 400. - **The fix restores the thinking block with Qwen's stock condition** - `{{ if (and $.IsThinkSet (and .Thinking (or $last (gt $i $lastUserIdx)))) -}}`, with its `$lastUserIdx` loop - byte-for-byte the Go template this repo shipped before its preserve-thinking change. Reasoning from assistant turns before the latest user message is still dropped; the turn in progress, a tool-call chain included, keeps its own; Ollama detects thinking support again, the capability lists tie, and the Go template wins. No blob change: the Go template is its own file. - **A first attempt restricted the block to a trailing assistant message** (`$last` only). It was published briefly and was stricter than Qwen stock: on Ollama it also dropped a live tool-call chain's reasoning once the tool result followed. Caught in review and replaced by the stock condition above. - **Verified in an isolated Ollama 0.33.3 daemon against the published blob**, with the stock condition: `selected=go_template go_template="[completion tools thinking]"`; a first-turn tool call at `reasoning_effort: "high"` - the value that used to return 500 here - returns exactly one call; reasoning from a turn before the latest user message is dropped - **187 prompt tokens with a prior `thinking` field and 187 without** - while a live tool-call chain keeps it - **369 with, 324 without** - each request on a fresh load; and `reasoning_effort: "none"` switches thinking off on this path. - The `Modelfile` now explains why the block is load-bearing, and the README gains a **Which template Ollama runs** section, a Known limitations entry for the duplicate-call behaviour, and a correction to **Reasoning effort**: on Ollama's default path the Go template runs, and it did not read the tier at the time (it does now; see "Changed (reasoning effort now reaches the prompt on Ollama)"). ### Fixed (an unfamiliar `reasoning_effort` no longer fails the request) - **`reasoning_effort: "high"` returned HTTP 500 - so did most other values.** Ollama 0.33.3 was rendering the Jinja template embedded in this GGUF for this model (its log reported `selected=gguf_chat_template` - a side effect since reverted, see above), so the template's own validation ran on every `/v1/chat/completions` call. Upstream accepted only `xhigh`, `medium` and `low`, and raised on everything else: `high` - the value most OpenAI-compatible clients send - plus `minimal`, `none`, `max`, `ultra`, and an explicit JSON `null`, which `reasoning_effort|default('xhigh')` does not catch because `default()` replaces only an *undefined* value. A runtime that passes the raw value (llama-server) hit every one of those; Ollama folds `xhigh` and `ultra` into `max` and `minimal` into `low` and sends no value for `none` or `null`, so there the failures were `high`, `xhigh`, `max` and `ultra`. - **The template now normalises instead of validating.** Values are lower-cased and trimmed; `low`/`minimal` select low; `medium` selects medium; `none` (or `false`) switches thinking off for that render, including the closed `` prefill; everything else - `high`, `xhigh`, `max`, `ultra`, empty, `null`, anything unrecognised - uses upstream's xhigh default. Nothing raises. - **Verified on the real engine, not just in a Python renderer.** An isolated Ollama 0.33.3 daemon running the re-stamped blob selected `gguf_chat_template` - the path a normal install took until the Go template's thinking block was restored (see above) - and returned HTTP 200 for `high`, `max`, `none`, `null`, `minimal` and `medium`. - **Mixed case is Ollama's call, not the template's.** Ollama's `/v1` layer rejects anything outside its eight lower-case values with HTTP 400 (`invalid reasoning value: "HIGH"`) before the template runs. The template itself lower-cases, so a runtime that forwards the raw value gets it accepted (llama.cpp does, as later tested; LM Studio is untested). - **`scripts/check_chat_template.py` pins it.** Every value a client might send - the eight Ollama forwards, explicit `null`, empty string, garbage, numbers, booleans - must render; `high`/`xhigh`/`max`/`ultra` must select the top tier, `low`/`minimal` the low tier, `medium` the medium tier, and `none` must switch thinking off. - Bundled GGUF re-stamped; only `tokenizer.chat_template` changed, all 851 tensors carried through untouched. ### Changed (reasoning is no longer replayed across turns) > Partly superseded later in this release: the template now ignores `preserve_thinking` (it never read `preserve_reasoning`), so the opt-in below no longer works; see "Fixed > (llama-server replayed earlier reasoning anyway)". The README gives the template edit that > restores replay. - **Traces from earlier assistant turns are dropped from the prompt.** Only the current turn generates a `` block. This repo previously preserved them deliberately — a documented divergence from Qwen stock — so the model could see how it reached its previous answers. The cost was context: every retained trace stayed in the prompt and prefill grew with it. - ~~**Measured A/B on this model**, same 3-message request, only the template differing: 192 prompt tokens without replay vs 221 with.~~ **Retracted** - see "Ollama runs the Go template again" above. Removing the block had also switched Ollama to a different template, so that comparison did not isolate the thinking block. Measured again with one template and a fresh load per request: **187 prompt tokens with a prior `thinking` field and 187 without**, on both models; a live tool-call chain keeps its trace (**369 with, 324 without**). - Changed on **both** runtimes, so they stay consistent: - Ollama — the `{{ if (and $.IsThinkSet .Thinking) }}` block is gone from the `Modelfile` TEMPLATE and the bridge `template` (both, so `check_bridge_sync.py` stays green). Byte-equivalent to before for any turn that carried no trace. **Deleting it outright was a mistake** - it is back, with Qwen's stock condition; see "Ollama runs the Go template again" above. - llama.cpp — `chat_template.jinja` now defaults `preserve_thinking` to **false** where upstream defaults it true. Both bundled GGUFs were re-stamped so the embedded template matches; only `tokenizer.chat_template` changed, all 851 tensors carried through untouched. - **Reasoning is still returned to you** — Ollama's `thinking` field, llama.cpp's `reasoning_content`. It is simply not fed back in on the next turn. - **Opt back in** with `preserve_thinking=true` (or `preserve_reasoning=true`) as a template kwarg on llama.cpp; on Ollama it is a template edit - change the thinking block's condition to `(and $.IsThinkSet .Thinking)`, as the README shows. - **Reasoning emitted after the last real user query is still kept**, on both runtimes, so a live tool-call chain does not lose its trace mid-flight. - `scripts/check_chat_template.py` grows to **19 assertions**; the four new ones pin exactly this: dropped by default, replayed on explicit opt-in, dropped on explicit opt-out, and the live tool chain keeping its own trace. ### Fixed (leftovers caught by a post-change verification sweep) - **The "Embedded-jinja path" section still described the bundle as carrying upstream's XML-defaulting template.** True before the blob was stamped, false after: the bundled GGUF embeds this repo's `chat_template.jinja`, so that path works with no flag. The section now attributes the XML default to a quant pulled straight from upstream, which is where it actually applies. - **The shipped template and the blob it is stamped into had drifted.** Two problems in `chat_template.jinja` itself: its invalid-value error still named `xml` as the default after this repo changed the default to `auto`, and the header advertised `--chat-template-kwargs` as an equivalent alternative. Both fixed, and both bundled GGUFs re-stamped so the embedded template is byte-identical to the repo file again (tensor data untouched — all 851 tensors carried through unchanged, only the `tokenizer.chat_template` KV differs). - **`scripts/verify_arch.py` now asserts that too.** Nothing caught the drift: the template is data, the blob is an LFS pointer in a fresh clone, and a stale embedded copy silently reinstates the tool-calling failure for every loader that reads the GGUF rather than an external file. `verify_arch.py` already loads the real weights, so it now compares `tokenizer.chat_template` against `chat_template.jinja` and prints the re-stamp command on mismatch. Verified in both directions against a correctly-stamped blob and a stale one. - **`--chat-template-kwargs` is not an alternative in this repo, and the docs said it was.** `tool_call_format` is added *by* `chat_template.jinja`; the stock upstream template here has no such setting, so passing the kwarg against it does nothing. That shortcut works only for the Janus sibling, whose base template already carried the switch. Corrected in the template header and the CHANGELOG. - The claim that pinning `xml` refuses string arguments "with upstream's message" was also wrong — upstream had no such check and simply died with a raw `items` TypeError; this template raises an explicit message instead. - **`scripts/cap_ctx.sh`'s LFS guard could never fire.** A fresh clone leaves the bundled GGUF on disk as a ~136-byte git-LFS *pointer*, and `[[ -f ]]` is true for that pointer — so the check passed and `ollama create` was handed a text file. It now checks the GGUF magic bytes, and the remedy it prints is one that works (`git lfs pull`, or `load_bundle.sh`'s `.cache/` path) rather than an operation `load_bundle.sh` never performs. - **The card claimed `load_bundle.sh` "smudges the LFS pointer".** It does not — it downloads into `.cache/` and re-points its own variable, leaving the working-tree pointer untouched. Corrected in the README (and the Makefile help where it said the same). - **Leftovers from the superseded tool-call diagnosis removed.** `strip_mtp.py`'s docstring still described llama.cpp's "--jinja grammar" rejecting the XML dialect and returning an empty completion; that account was retracted. It now describes the real failure (the template raises on JSON-string `arguments`; llama-server returns a 500 on the second turn). - **`ollama/ollama#14575` follow-through.** The Vision section still concluded "until the Ollama upstream issue is fixed"; the issue is closed, so the text now says the table has not been re-verified since rather than asserting it is still broken. - **Sibling-only fixes back-ported.** The 32 GB "still OOMs" contradiction and the 24 GB "full Q4 offload" claim were corrected in Janus (and in this repo's README hardware table) but survived here in the `/v1` callout and the `Modelfile` working-configurations block. Both now match the ~26 GB the card computes. - **`scripts/fetch_vision.sh`** quoted the retired ~931 MB projector size and printed a follow-up command loading the raw upstream quant; both corrected. - **`examples/llama_cpp_quickstart.py` and `examples/llama_cpp_vision.py`** still pointed `--gguf` at `Qwen3.8-27B-heretic-ara.Q4_K_M.gguf`, the MTP-carrying upstream file the same files warn against. They now use the bundled blob. The vision example also quoted the retired `qwen35moe` arch stamp and a stale "until that lands". - **`check_bridge_sync.py`'s commit citation** was left reading "33458f7 / 70ccef1" by the earlier fix; `70ccef1` does not exist, so it now cites only the real commit. ### Changed (the bundled blob carries the fixed chat template) - **`Thanatos-27B.Q4_K_M.gguf` was restamped.** Shipping `chat_template.jinja` beside the weights never reached LM Studio, Jan, KoboldCpp or llama-cpp-python: they read the template *embedded in the GGUF* and have nowhere to pass `--chat-template-file`, so on those loaders tool calling still died with the upstream template's `Can only get item pairs from a mapping` — a llama-server 500 on the second turn of every tool call. - The source was **this repo's own published blob** (sha256 `188dfa6bc3210b29c7cb08073b2431865305a40c79d3cbbcc5a7c1ef18f88710`, verified before use), so what ships is exactly what shipped before with one metadata key changed. **All 851 tensors are byte-for-byte identical** — checked by hashing each tensor in both files — so there is no re-quantization and no quality change. The file grows by 4,928 bytes, the size difference between the two templates. New sha256: `6ed2d5e05d74abfc0dc38135fc2169fee3df1f80e2f491717951d1200db168ba`. - Verified before publishing: 14/14 Architecture claims via `scripts/verify_arch.py`; loads in Ollama as `qwen35` / 26.9B dense with `tools` and `thinking`; and a full tool-call round-trip on `/v1/chat/completions`, including the replay turn whose string `arguments` is the failure this fixes. - `--chat-template-file` is now only needed for a quant pulled straight from upstream. Docs updated accordingly. ### Fixed (numbers and references the audit caught) - **Sibling references updated for Janus's blob swap.** The Janus-vs-Thanatos note said Janus's bundle "is still the old ~21 GB MoE quant and the re-quantization is pending"; that landed, so both siblings now ship what their cards describe. The Q4_K_M size row is also exact rather than rounded to a shared "~17 GB": Thanatos ~16.5 GB, Janus ~17.3 GB. - **`scripts/check_bridge_sync.py` docstring corrected** — it cited commit `70ccef1`, which exists in neither repo; the real one is `33458f7`. - **`make help` described the filename guard backwards.** It said "block dot-pattern footgun"; the guard bans the *dash* form, because mradermacher names heretic-ara quants with a dot. - **`make clean` now sweeps `*.stripped.gguf`**, the `build.sh` / `strip_mtp.py` intermediates that `.gitignore` already knows about and `clean` was leaving on disk (~17 GB each). - **The Janus-vs-Thanatos table claimed the heretic-ara source weights have no MTP head.** They do — read straight from the base GGUF's metadata: `nextn_predict_layers = 1`, `block_count = 65`. Three other places in this same README already said so. Both siblings ship MTP-preserved sources and both bundles are stripped. - **Hardware table promised "full Q4 offload" on a 24 GB card**, contradicting the same section's ~26 GB total at the shipped default. Now says partial offload, with the `num_ctx` trim needed for a full one. - **`imatrix` frontmatter tag removed.** The bundle comes from mradermacher's *static* GGUF repo, not the `i1` importance-matrix one. - **`CITATION.cff` un-stalled** — it was pinned at `0.1.0` / `2026-05-02` while the CHANGELOG had reached `0.6.0` / `2026-05-19` and a full base rebase had landed since. - **`ollama/ollama#14575` is no longer described as open** — it closed as completed on 2026-08-04 (checked against the GitHub API). The vision notes now say so and flag that the loader table has not been re-verified since. - **`transformers>=4.51` floor dropped** from the examples: it predates Qwen 3.8 by a major version. ### Added (chat template is stamped into the GGUF at build time) - **`scripts/strip_mtp.py` gained `--chat-template FILE`**, which replaces the GGUF's `tokenizer.chat_template` in the same pass that drops the MTP block. `scripts/build.sh` now passes `chat_template.jinja` automatically when the file is present. This matters because `--chat-template-file` is a llama.cpp flag: LM Studio, Jan, KoboldCpp, Open WebUI and llama-cpp-python all read the template **embedded in the GGUF** and have nowhere to put that flag, so they were still getting the upstream XML tool-call form, which raises on the JSON-string `arguments` an OpenAI-compatible client replays — a llama-server 500 on the second turn of every tool call. A GGUF built here now carries the working template, and those loaders need no flags at all. - The script stays a strip tool: with `--chat-template` on an already-MTP-clean input it rewrites the file purely to swap the template (the usual hardlink passthrough cannot carry one), and still hardlinks when neither a strip nor a stamp is needed. Tensor data is copied byte-for-byte in every path — no re-quantization, no quality change. An empty template file is refused rather than stamped. - Verified against synthetic GGUFs across all six paths: MTP-strip alone; MTP-strip plus stamp; clean input plus stamp (rewrites, block_count preserved, tensor bytes intact); clean input with no stamp (hardlink, same inode); empty template file rejected; and this repo's real `chat_template.jinja` stamped and read back byte-identical. - Documented in the README: a locally built GGUF needs no flag, and there is a one-liner to restamp a GGUF you already have. ### Fixed (provenance, and a CLI-detection footgun) - **`base_model_relation: finetune` → `quantized`.** Nothing is finetuned in this repo; it repackages and quantizes an upstream abliteration. A new **Provenance** note under the header now says so plainly and reframes the `Teacher:` field and the frontmatter `datasets:` list as the *inherited lineage claimed by the upstream base* — attribution, not a training record. Nothing here reproduces them and no evaluation in the card measures them. - **Dataset owner casing corrected** to `Crownelius/` on the two entries that had it lowercase; the lowercase form 307-redirects on the Hub. (Verified against the Hub API: `Crownelius/Crow-Heretic-TeichAI-Unified` and `microsoft/rStar-Coder`, `peteromallet/dataclaw-peteromallet`, `openbmb/UltraData-Math` resolve 200; `Crownelius/Opus-4.7-Reasoning` returns 401, which the Hub also returns for a private or missing repo (a gated one answers 200), so it is not publicly verifiable.) - **`scripts/build.sh`, `scripts/load_bundle.sh` and `scripts/fetch_vision.sh` no longer trust the bare name `hf`.** All three probed `command -v hf` *first*, but `hf` is ambiguous: Arch's `hf` package ships an unrelated hidden-file utility under that name. On such a host every download path selected the wrong binary and failed with an argument error that says nothing about Hugging Face. A shared `detect_hf()` now prefers the unambiguous `huggingface-cli` and accepts `hf` only when its `--help` identifies it as Hugging Face's. Confirmed on a machine with Arch's `hf` installed: the old probe selected `/usr/bin/hf`, the new one rejects it and reports that no HF CLI is present. The two-armed `case` that dispatched on the CLI name is gone — both spell `download` identically. - **`scripts/check.sh` guard 6 is now multi-line aware.** It was a line-anchored grep, so a command substitution split across backslash-continued lines was invisible to it — and one had been living in `scripts/heal_hf_pull.sh:86` the entire time the check reported green. Rewritten to join continuations into logical lines before matching, and verified against an injected decoy. - **`scripts/heal_hf_pull.sh` — the silent-exit footgun that `385ed94` fixed at line 67 was still present at line 86.** `MANIFEST="$(find ... 2>/dev/null | head -1)"` spans three lines; under `set -e` + `pipefail` a failing `find` (missing manifests directory) killed the script before the `[[ -z "${MANIFEST}" ]]` check could report anything actionable. Split using the same recipe as the earlier fix. ### Fixed (vision path pointed at a GGUF that cannot load) - **Every documented llama.cpp and vision command loaded the raw upstream quant.** `-m Qwen3.8-27B-heretic-ara.Q4_K_M.gguf` is the mradermacher file, which — as this card's own Architecture section says — still carries the MTP / NextN layer that stock llama.cpp aborts on. The bundled `Thanatos-27B.Q4_K_M.gguf` is that same quant already stripped, so the README's three vision recipes, the Vision stack listing, `examples/llama_cpp_vision.py` and `examples/README.md` now use the bundle. Besides fixing the crash this saves users a redundant ~17 GB download. A callout explains the MTP strip for anyone who does want an upstream quant. - **mmproj size corrected `~931 MB` → `~928 MB`.** The f16 projector is 927,607,136 bytes; 931 MB is the *BF16* projector in the Janus sibling's repo. - **Smallest published quant corrected `Q3_K_S` → `Q2_K`** in the README and the `Modelfile`. mradermacher publishes Q2_K (~10.9 GB) and IQ4_XS below the claimed Q3_K_S floor (~12.3 GB); `scripts/build.sh` already listed them correctly. ### Fixed (tool calling under llama.cpp / llama-server) - **`chat_template.jinja`** — the base model's own embedded template, patched only where the tool-call wire format is decided. It gains a `tool_call_format` setting — `auto` (default), `json`, `xml` — mirroring what the Qwen 3.8 "safe" template does and what the Janus sibling pins. `reasoning_effort`, the vision handling and every other upstream behaviour are unchanged. - **The failure is on the second turn, not the first.** Every OpenAI-compatible client hands assistant tool calls back with `function.arguments` as a JSON **string**. The XML tool-call form the base template defaults to needs the individual key/value pairs, and a chat template has `tojson` but no inverse, so it cannot decode one. The base template applies the `items` filter to the string and dies. Reproduced by rendering the stock template from the base model's own GGUF metadata: ```text Can only get item pairs from a mapping. ``` Under `llama-server` that raise is a **500**: the model emits the tool call, the result is fed back, and the next request dies — an agent loop halts after exactly one call. - **`auto` is lossless.** It renders the prompt in JSON form only when a historical tool call carries string arguments, and is **byte-identical to upstream's XML** when every argument is a mapping — verified by rendering both templates over the same mapping-argument conversation and comparing output. `tool_call_format='xml'` still refuses string arguments — with an explicit message rather than upstream's raw `items` TypeError — and an unknown value is still rejected. - The kwarg is NOT an alternative in this repo: `tool_call_format` is added *by* this file, and the stock upstream template has no such setting, so passing `--chat-template-kwargs` against it does nothing. (That shortcut works only for the Janus sibling, whose base template already had the switch.) The file exists for loaders with nowhere to pass kwargs (LM Studio, Jan, KoboldCpp), and is what `scripts/build.sh` stamps into the GGUF it builds. - **The Ollama path is unchanged.** `Modelfile`, `template`, `system` and `params` are untouched — tool calling there already worked, and `/api/chat` and `/v1/chat/completions` were re-verified end to end (single call, streaming, and the tool-result round-trip) before this change. - **`scripts/check_chat_template.py`**, wired into `scripts/check.sh` as check 8 (so `make check` and the pre-commit hook cover it), renders the template and asserts 15 properties — that string arguments do not raise, that they render as JSON, that mapping arguments still render, that each `tool_call_format` value behaves, that both `` prefill directions are right, and that vision pads survive. The template is data, not code, so `py_compile` and `pyflakes` never look at it and a one-word edit restores the breakage silently. #### Correction to the first two attempts at this fix The initial commits shipped a hand-written template and a **wrong diagnosis** — that llama.cpp selected a Hermes-2-Pro grammar which disagreed with the template's XML instructions and terminated the decode, yielding an empty completion. That is not what happens. The real failure is the raise above, on the replay turn, surfaced as a 500. Both earlier versions were also derived from the wrong source: the template read out of this repo's *pre-rebase* GGUF, which is a different Qwen generation from the current base. Reading the real embedded template out of each base showed the two siblings do not even share one — Janus's is `qwen3.8-safe-v3.1` (28,277 chars, with the `tool_call_format` switch already built in), Thanatos's is the stock Qwen template (8,953 chars, XML only, but carrying `reasoning_effort`). Shipping one identical hand-written file to both discarded the safe-v3.1 template entirely on one side and `reasoning_effort` on the other. Each repo now carries a patch of *its own* base template. ### Changed (num_ctx default → 65,536) - **Baked `num_ctx` default lowered `262,144 → 65,536`** in `params` and the `Modelfile` (bridge-synced). 262,144 remains the model's **native window** and the `Context:` header field is unchanged — what moves is only the default the model loads at. Rationale: the 262,144 default allocated a ~16 GB KV cache for ~38 GB total, which does not fit a 32 GB host, so the documented first step for most users was to override it back down. 65,536 is ~4 GB KV / ~26 GB total and loads without hand-tuning, while raising it toward the native window stays a one-line change for hosts with the RAM. - The TL;DR heads-up, `/v1` callout, Hardware section, hardware table rows, `Modelfile` footprint math and `scripts/cap_ctx.sh`'s header were all reframed so **65,536 reads as the default, 262,144 as the native window, and 1,010,000 as the extensible YaRN ceiling**. `scripts/verify_arch.py` is untouched: it asserts the GGUF's `context_length` metadata (262,144), which is a property of the weights, not of the baked default. ### Fixed (documentation — `/v1` silently defeats the sampling defaults) - **The card promised "loop insurance" that the OpenAI-compatible endpoint does not deliver.** `Recommended sampling` describes the shipped profile as "`temperature 1.0`, no `top_k`, with `top_p 0.95` + `repeat_penalty 1.05` kept as loop insurance". Ollama's `/v1/chat/completions` layer applies OpenAI's own default of `top_p 1.0`, overwriting the `0.95` baked into `params` and the `Modelfile`. Measured on this model, same request, same tag: `top_p = 0.950` on `/api/chat`, `top_p = 1.000` on `/v1/chat/completions`. - **Combined with `top_k 0` (disabled by design), a `/v1` request therefore samples with no truncation at all at `temperature 1.0`** — every one of the 248,320 vocabulary entries live at its raw probability. The sampling values are unchanged (the warm, `top_k`-free profile is a deliberate Fable match); what is added is a callout saying the guard exists only on the native API, and that `min_p` and `top_k` pass through `/v1` untouched while `top_p` does not. ### Changed (thinking is now preserved across turns) > Superseded later in this release: earlier turns' reasoning is no longer replayed; see > "Changed (reasoning is no longer replayed across turns)" and "Fixed (llama-server > replayed earlier reasoning anyway)". - **The chat template renders the `` block of every assistant message that carries one, not just the current turn.** The guard `(or $last (gt $i $lastUserIdx))` — Qwen's stock behaviour, which drops reasoning from every earlier turn — is removed, leaving `(and $.IsThinkSet .Thinking)`. The four-line `$lastUserIdx` preamble existed only to feed that guard and is deleted with it. Applied to **both** `template` and the `Modelfile` `TEMPLATE` block, so the HF Ollama bridge and local `ollama create` builds stay in sync (`check_bridge_sync` green). - **Measured effect.** Two tags built off the same weights differing only in template, given a 3-message conversation whose first assistant turn carried a ~240-word thinking block: **181 prompt tokens with the old template, 464 with the new** — the retained trace, rendered back into the prompt. - **The cost is context, and it compounds.** Every retained trace stays in the prompt, so reasoning-heavy multi-turn sessions grow much faster than upstream and prefill time grows with them. Documented in the card (`Thinking is preserved across turns`) and in a `Modelfile` comment marking the divergence from Qwen stock, so it is not "fixed" back by mistake. ### Changed (sibling comparison retired — Janus is no longer a MoE) - **The `Why a 27B variant?` section is gone, replaced by `Janus vs Thanatos`.** `FoolDev/Janus-35B-HERETIC` rebased off the Qwen 3.6 35B-A3B MoE onto the same dense Qwen 3.8 27B family this model uses, so every dense-vs-MoE argument the card made about its sibling became false: "instead of the 35B-A3B MoE. Smaller, easier to deploy", "the lighter sibling to Janus-35B", "More capacity, more memory pressure", and a 12-row comparison table whose Architecture / Total params / Active params / Layers / Hidden size / GGUF size / Min host memory rows all described a model that no longer exists. The replacement compares what actually differs — **ARA vs MPOA-style abliteration**, immediate base, and whether the source weights carry an MTP head — and states plainly that everything else is shared. - **Dropped a dangling benchmark.** The old section cited "~27 tok/s for the MoE 35B at ~Q4" as a contrast to this model's ~12 tok/s; that figure was measured on a build the sibling no longer ships, so it is deleted rather than carried over. This repo's own measured figures (~12.3 Q3_K_S, ~9.3 Q4_K_M) are untouched. - **Sibling badge, intro paragraph, `llama.cpp` arch note, Hardware lead-in, Related models row, vocab bullet, `Modelfile` template comment and `CITATION.cff` reference** all reworded off "35B" / "MoE". The `Janus-35B-HERETIC` **slug is deliberately unchanged** everywhere — renaming it would break every published link — so the card now notes it as a pre-rebase artifact where it appears. - **Known limitations** no longer claims "Slower per token than the 35B-A3B sibling … if you optimize for tokens-per-second, the MoE wins." It now states the dense cost honestly and notes the sibling offers no speedup. ### Fixed - **`scripts/cap_ctx.sh` no longer cites the retired baked default.** Its header explained the helper by saying OpenAI `/v1` loads at `num_ctx 1010000` and OOMs on <=~48 GB hosts — stale the moment the baked default dropped to 262,144 in the same release. The rationale still holds at the new default (~38 GB total), so the header now says that instead of a number the repo no longer ships. Matches the same fix in the sibling `FoolDev/Janus-35B-HERETIC`. ### Changed (num_ctx default → the 262,144 native window) - **Baked `num_ctx` default lowered `1,010,000 → 262,144`** in `params` and the `Modelfile` (bridge-synced). 262,144 is the model's native trained window — safe, no RoPE degradation — while the card still advertises **extensible to 1,010,000** via the opt-in YaRN recipe (and the `Context:` header field is unchanged). At the new default the footprint is ~38 GB total vs ~84 GB at the 1.01M ceiling; the TL;DR, Hardware section, `/v1` callout, and Modelfile footprint math were reframed so 262,144 reads as the default and 1,010,000 as the extensible ceiling. ### Changed (base rebased → heretic-org Qwen 3.8 27B heretic-ara) - **Rebased the card from the Qwen 3.6 27B Heretic v2 base to the Qwen 3.8 27B `heretic-ara` base.** The immediate base moves from `llmfan46/Qwen3.6-27B-uncensored-heretic-v2` to [`heretic-org/Qwen3.8-27B-heretic-ara`](https://huggingface.co/heretic-org/Qwen3.8-27B-heretic-ara) — a genuine Heretic-tool abliteration of `Qwen/Qwen3.8-27B` via the Arbitrary-Rank Ablation (ARA) method (0/100 refusals, KL ≈ 0.05). The base **author necessarily changed**: `llmfan46` has published no 3.8 release. GGUF quants now come from [`mradermacher/Qwen3.8-27B-heretic-ara-GGUF`](https://huggingface.co/mradermacher/Qwen3.8-27B-heretic-ara-GGUF), the MTP-preserved variant from `cygnal/Qwen3.8-27B-heretic-ara-Q4_K_M-MTP-GGUF`, and the vanilla reference projector/base from `unsloth/Qwen3.8-27B-GGUF`. - **The architecture is identical across 3.6 → 3.8** (both `model_type: qwen3_5`): 64 layers, hidden 5120 / FFN 17408, 24 Q / 4 KV heads at head_dim 256, Gated-DeltaNet 48 V / 16 QK at head_dim 128, partial RoPE 0.25, vocab 248,320, 262,144 native context. So this is a **relabel, not an architecture change** — the `general.architecture: 'qwen35'` stamp, `scripts/verify_arch.py` expected values, and the `template` / `system` / `params` bridge files are all unchanged; `verify_arch` and `check_bridge_sync` stay green. - **Filename convention flipped dash → dot.** heretic-ara GGUFs are named `Qwen3.8-27B-heretic-ara.${QUANT}.gguf` (dot separator) where the old llmfan46 quants used dashes. `scripts/build.sh` (`GGUF_NAME`), `scripts/fetch_vision.sh` (mmproj `mmproj-f16` / `mmproj-Q8_0`, default precision `f16`), the `Makefile` (`PRECISION`, `clean` glob) and the `scripts/check.sh` footgun-grep were retargeted to the dot convention. Q3_K_S (~12 GB) is now published directly, so the old "no Q3_K_S in the repo" caveat is dropped. - **Docs + metadata retargeted throughout**: frontmatter `base_model` and the `qwen36 → qwen38` tag, the Base badge (`Heretic_v2 → Heretic_ARA`), the README (subtitle, TL;DR, What's-here, Architecture label, Vision, MTP → cygnal, arch-stamp family note extended to `3.5 / 3.6 / 3.8`, related models, credits), `CITATION.cff` (title / abstract / references / keyword), and the `Modelfile` header + source list. The Janus-35B-A3B cross-references stay on 3.6 (the sibling was not rebased), and the historical qwen35/qwen36 arch-stamp saga is preserved as-is. - **Bundled blob swapped to the MTP-stripped 3.8 Q4_K_M.** The current heretic-ara GGUFs (`mradermacher/Qwen3.8-27B-heretic-ara-GGUF`) ship **with** the MTP/NextN layer as an extra block (`block_count` 65), which today's stock llama.cpp / Ollama can't load — the loader treats the NextN block as a hybrid layer and aborts with `missing tensor 'blk.64.ssm_conv1d.weight'` (verified on llama.cpp + Ollama 0.30.8). So `Thanatos-27B.Q4_K_M.gguf` is now the heretic-ara Q4_K_M run through `scripts/strip_mtp.py` (drop block 64 → `block_count` 64, 851 tensors; already `qwen35`-stamped, no re-stamp). Verified: `verify_arch` green, and it loads + generates on llama.cpp (Vulkan, 8060S iGPU). Benchmarks carry over (same architecture + quant size), pending a re-measure with `make bench` (superseded later in this release: they do not carry over; see the throughput entry). ### Added - **`/v1` cap-ctx helper + strip_mtp/build.sh hardening.** New `scripts/cap_ctx.sh` (wrapped as `make cap-ctx`) bakes a small-`num_ctx` local tag (`num_ctx`→4096, `num_batch` 256, reusing the Modelfile's TEMPLATE/SYSTEM) so OpenAI `/v1` clients — which can't override the baked `num_ctx` and OOM on small hosts — work in one command (verified: the tag loads via `/v1` at CONTEXT 4096, no OOM). Also hardened `scripts/strip_mtp.py` to refuse stripping unless the target block actually holds a NextN tensor (guards against dropping a real transformer layer on a malformed upstream), and documented the `.stripped.gguf` sibling in `build.sh`'s header. - **Header spec line now surfaces context length.** Added a `Context: 262,144 native / 1,010,000 extensible` field to the card's header spec row (alongside Architecture / Parameters / Base / …), so the native + YaRN- extensible window is visible at a glance. The `num_ctx 1010000` default and the Architecture / Hardware detail are unchanged. - **`scripts/strip_mtp.py` — MTP/NextN layer stripper for `qwen35` GGUFs.** Drops the extra `blk..*` MTP block, sets `block_count` back to 64, and removes `nextn_predict_layers`; kept tensors are copied byte-for-byte (no re-quant). Needed because current heretic-ara GGUF conversions bundle the MTP layer, which stock llama.cpp / Ollama can't load — see the base-rebase entry above. It is a conditional no-op (hardlink, no extra disk) on already-clean quants, and **`scripts/build.sh` now runs every fetched quant through it**, so `make build QUANT=` produces a loadable model — not only the bundled Q4_K_M. Companion to `scripts/rename_arch.py` in the GGUF-surgery toolset. - **`scripts/check_bridge_sync.py` now guards the example clients' system prompt** (ported from the MoE sibling `FoolDev/Janus-35B-HERETIC`). Beyond the Modelfile↔bridge check, it parses each `examples/*.py` and asserts any module-level `*_SYSTEM` constant matches the root `system` verbatim; the vision client is skipped (deliberate image-specific prompt). Wired into `scripts/check.sh` and the pre-commit hook, so drift is caught before it lands. - **Frontmatter tags `reasoning` + `distillation`.** The card is positioned reasoning-first and distilled (and `CITATION.cff` already lists both as keywords), but neither was a searchable Hub tag — added both. Parity with the Janus sibling. - **`CITATION.cff` now carries `version` + `date-released`.** Added `0.1.0` / `2026-05-02` (mirroring the CHANGELOG release anchor) so the HF "Cite this model" widget generates a dated, versioned citation. Parity with the Janus sibling. ### Changed (default sampling matched to the Fable teacher) - **Default sampling raised to a Fable-matched profile** in `params` and the `Modelfile` (bridge-synced): `temperature 0.6 → 1.0`, `top_k 20 → 0` (Claude uses no top-k). `top_p 0.95` and `repeat_penalty 1.05` are kept as guardrails against reasoning-loop degeneration (open weights + a forced `` block need them, unlike Claude's sampler). In parity with the MoE sibling `FoolDev/Janus-35B-HERETIC`. The former 0.6/0.95/20/1.05 profile remains in the README "Recommended sampling" table as the tighter-reasoning override. ### Changed (context-length ceiling raised to Qwen's 1,010,000 extensible max) - **Advertised context is now the precise 262,144-native / 1,010,000-extensible pair, and the baked default rises to match** (parity with the MoE sibling `FoolDev/Janus-35B-HERETIC`). The card previously rounded the extensible window to `~1 M` and baked `num_ctx 1000000` (a Fable-teacher budget match); both now use the exact Qwen 3.6 figures — **262,144 native, extensible to 1,010,000 tokens with YaRN**. The baked default moves `1000000 → 1010000` in `params` and the `Modelfile` (bridge-synced), the opt-in YaRN recipe is retuned to `--rope-scale 3.853` (≈ `1010000 / 262144`) at `-c 1010000`, and the theoretical KV footprint at the default is recomputed to ~62 GB. ### Fixed - **Post-rebase audit fixes (multi-agent card audit).** (1) `examples/llama_cpp_vision.py` `--mmproj` help named a non-existent `mmproj-BF16.gguf` — a bare reference the rebase sed missed (the heretic-ara repo publishes only `mmproj-f16` / `mmproj-Q8_0`), now `mmproj-f16.gguf`. (2) `examples/README.md` listed `Q3_K_M ~12 GB` (that's the Q3_K_S size; Q3_K_M is ~13 GB everywhere else) → `~13 GB`. (3) The `Modelfile` bench-provenance note still said `rebundle pending` after the rebundle shipped → aligned to the README's "figures carry over — re-measure with `make bench`". - **Ignore `build.sh`'s `*.stripped.gguf` intermediates.** A `make build GGUF_PATH=` run produces `Thanatos-27B.Q4_K_M.stripped.gguf`, which the broad `!Thanatos-27B.*.gguf` un-ignore rule left trackable; re-ignored it so it can't be accidentally committed. (Verified `make build` end-to-end while here.) - **OpenAI `/v1` examples now warn about the `num_ctx` OOM on small hosts.** `/v1/chat/completions` has no `num_ctx` knob, so it loads at the baked `num_ctx 1010000` and OOMs on ≤~48 GB hosts (the KV cache alone wants ~62 GB) — but the examples showed `/v1` with no caveat. Added a callout beside the OpenAI-compatible examples: use `/api/chat` with `"options": {"num_ctx": 4096}`, or `/save` a small-context local tag for OpenAI clients (`/v1` can't pass the override inline). - **Banner regenerated for the 3.8 rebase.** The `banner.svg` subtitle still read `Qwen 3.6 Heretic` (the card embeds the SVG, so this was visible on the live page); corrected to `Qwen 3.8 Heretic` and re-rendered `banner.png` from it (`rsvg-convert`, 766×154). `dense-flow.svg` / `bmc-banner.svg` carry no version text, so they and their renders are unchanged. - **`llama_cpp_quickstart.py` CPU-only example no longer passes `--gpu-layers 99`** (parity with the Janus sibling). The `examples/README.md` llama-cpp-python block installed the CPU-only wheel but then ran with `--gpu-layers 99` — a silent no-op on that build; the flag moved into the GPU-rebuild note. - **Modelfile footprint math now agrees with the README and the spec table.** The `Modelfile` hardware comment still carried the pre-reconciliation ~29 GB weights+compute-graph anchor (`compute graph alloc ~12 GB`; `~91 GB` / `~45 GB` totals) while the README hardware paragraph and the Janus-vs-Thanatos comparison table (`Min host memory @ Q4 / 8K ctx | ~22 GB`) had already moved to ~22 GB. Aligned the Modelfile to the ~22 GB anchor (graph `~12 → ~5 GB`; totals `~84 GB` at the default / `~38 GB` at 262144 native), completing the 2026-08-02 reconciliation (`08ae542`) that had missed this one file. - **Example accuracy brought in line with the Janus sibling.** Three lagging spots corrected: `llama_cpp_vision.py`'s `--mmproj` help said `mmproj-F16.gguf` (the rest of the file and Janus say BF16); `ollama_chat.py`'s docstring said "Current Ollama (0.24" (its own body and Janus say "0.24+"); and the `transformers` install floor was `>=4.45` in three places, below the Qwen 3.6 support floor Janus pins at `>=4.51`. - **Inference examples no longer 404 for HF-pull users.** The OpenAI-compatible examples hard-code `model: "thanatos-27b"`, the tag from the local build; a user who pulled via the TL;DR one-liner (`hf.co/FoolDev/Thanatos-27B-HERETIC`) hit `model not found`. Added a note to substitute the full tag (or `ollama cp`). Parity with the Janus sibling. - **Ollama vision-bug citations point at the open tracker.** Five live surfaces (README loader table, `examples/README.md`, `examples/llama_cpp_vision.py`, `scripts/fetch_vision.sh`, and the `Modelfile` header) cited `ollama/ollama#15898`, which was closed as a duplicate — readers clicking through saw "Closed" and could assume vision was fixed. All five now cite the open `#14575`. Parity with the Janus sibling. - **Hardware footprint figures reconciled.** The comparison table gave min host @ Q4/8K as ~22 GB while the Hardware section anchored weights+graph at ~29 GB (→ ~90 GB at 1M, ~45 GB at native) — a ~7 GB internal contradiction for the same config. Settled on the ~22 GB anchor (defensible for a ~17 GB GGUF): ~83 GB total at the 1M default, ~38 GB at the 262144 native window. - **`examples/ollama_chat.py` tool demo no longer crashes on an omitted arg.** `fake_weather(city, unit)` had no default for `unit`, so an Ollama tool call that omits the "required" arg raised `TypeError` mid-round-trip. Added `unit="celsius"` (parity with the Janus sibling). - **`scripts/rename_arch.py` defaults point the recovery direction.** Defaults were `--from-arch qwen35 --to-arch qwen36` (forward), opposite every caller (all pass qwen36→qwen35); a flagless run would mis-stamp the shipped qwen35 bundle to qwen36 and break loading. Flipped to qwen36→qwen35. - **Hardware/eval numbers no longer self-contradict.** The "No formal evaluation" note said "Numbers above are estimates," but the `make bench` tok/s figures (~12.3 Q3_K_S / ~9.3 Q4_K_M) are measured — reworded to carve those out. Parity with the Janus sibling. - **`examples/README.md` establishes its working directory.** The Setup section's first runnable command assumed a `cd examples` that was never stated (a fresh clone lands at repo root); added the note. - **Loader-table Ollama version reads `0.24+`.** The lone bare `Ollama 0.24` cell now matches the open-ended `0.24+` used elsewhere. - **`scripts/load_bundle.sh` + `scripts/heal_hf_pull.sh` size probes are portable.** Both sized files with GNU-only `stat -c '%s'` (errors on macOS/BSD `stat`, which needs `-f%z`) — switched to `wc -c`, identical on GNU and BSD. Parity with the Janus `load_bundle.sh` fix. - **`examples/llama_cpp_quickstart.py` system prompt restored.** Its `THANATOS_SYSTEM` had truncated to five behavior rules (dropped "or meta-commentary" and "or incomplete", and omitted the creative-writing and analysis/technical-help rules), diverging from the root `system`. Restored to the full seven-rule prompt, byte-identical to `system`. Parity with the Janus sibling. - **`examples/README.md` refreshed.** It claimed all four clients share the system prompt and the `0.6/20` sampling "defaults"; neither held — `llama_cpp_vision.py` uses a vision-specific prompt, and the shipped default is now Fable-matched `temp 1.0 / top_k 0`, so `ollama_chat.py` inherits `1.0/0` while the non-Ollama scripts pin the tighter `0.6/20` explicitly. States which profile each path actually uses (parity with the Janus fix). - **`scripts/check_bridge_sync.py` casts params by value shape, not a key whitelist.** `parse_modelfile` only numerically cast a hardcoded set of keys, so any other numeric param (e.g. `min_p`, `presence_penalty`) stayed a str and tripped a false "params drift" against its JSON number. Now casts every non-stop value `int()` then `float()` (mirroring `json.loads`); ported from the Janus sibling. - **`think=false` now suppresses reasoning.** The chat template unconditionally prefilled an open `` on the generation prompt, so requests passing `think: false` (or `ollama run --think=false`) still forced a reasoning trace that leaked into `content`. The prefill is now gated on `$.IsThinkSet`: a closed empty `` when thinking is explicitly off, an open `` otherwise. Verified on Ollama 0.30.8 (`think=true` keeps reasoning in the `thinking` field, `think=false` suppresses it, unset still reasons); `template` kept byte-synced with the `Modelfile` `TEMPLATE`. In parity with the MoE sibling `FoolDev/Janus-35B-HERETIC`. - **`scripts/smoke_test.sh` and `scripts/bench.sh` no longer OOM at the 1M default.** They sent no `options`, so they inherited `num_ctx 1000000` and its ~60 GB KV allocation; they now pin `num_ctx: 4096, num_batch: 256` (the documented constrained-host config). ### Changed (default context window 262144 → 1000000, a ~1M advertised ceiling) - **`num_ctx` default raised 262144 → 1000000** in `params` and the `Modelfile` (bridge-sync kept in sync), in parity with the MoE sibling `FoolDev/Janus-35B-HERETIC`. This targets a ~1M ceiling **above** the 262 144 native trained window. The bundled GGUF has **no YaRN rope-scaling** (`rope.freq_base 10M`, no `rope.scaling`), so positions past ~262K use untrained RoPE and degrade — 1M is a capability-first *advertised* ceiling, not a coherent context. `num_ctx` stays a per-run **ceiling, not a floor**, and KV grows to ~61 GB at 1M (q8_0, ~2 GB/32K → ~90 GB total; theoretical, extrapolated), so memory-constrained hosts must override `num_ctx` down at load (the 262 144 native window → ~16 GB KV / ~45 GB total; `num_ctx 32768` → ~2 GB KV). The Z13 measured data point stays at its as-benched `num_ctx 16384`; native-context checks (`verify_arch.py`) stay at 262 144 (the GGUF's trained window). ### Changed (default context window 16384 → 262144, the native max) - **`num_ctx` default raised 16384 → 262144** in `params` and the `Modelfile` (bridge-sync kept in sync), matching the MoE sibling `FoolDev/Janus-35B-HERETIC` — the model's full native context. The old 16K window exhausted mid-conversation on a reasoning-distilled model that emits long `` traces (triggering Ollama context-shift); the default is now capability-first — the whole 262 144-token window instead of a conservative slice. `num_ctx` is a per-run **ceiling, not a floor**, so memory-constrained hosts override it down at load (e.g. `num_ctx 32768` → ~2 GB KV). At the 262K default, KV cache is ~16 GB (q8_0) for a ~45 GB theoretical footprint, so most hosts must trim. The Z13 measured data point stays at its as-benched `num_ctx 16384`. (This default passed through 32768 before landing at the native max.) ### Changed (HF repo rename → FoolDev/Thanatos-27B-HERETIC) - **HF repo renamed `FoolDev/Thanatos-27B` → `FoolDev/Thanatos-27B-HERETIC`** via the HF UI. HF serves a 307 redirect from the old `FoolDev/Thanatos-27B` slug to the new canonical name, so existing pulls keep resolving. The **model's display name stays `Thanatos-27B`** — only the repo slug / URLs change. - **Repointed self-referential slugs to the new name** — README banner / `bmc-banner` / `dense-flow` image URLs, the `ollama run` / `ollama pull` / `git clone` / `hf download` quick-start and local-apps rows, `CITATION.cff` (`url:` + `repository-code:`), and the functional script defaults (`heal_hf_pull.sh` `TAG`, `load_bundle.sh` `REPO_ID`, `heretic_remote.sh` `MODEL`, `smoke_test.sh` usage, `check_bridge_sync.py`, `Makefile` help, `.gitignore` comment, `examples/*`) all now point at `FoolDev/Thanatos-27B-HERETIC`. The `heretic_remote.sh` `REPO_ID` upload target (`FoolDev/Thanatos-27B-Heretic-v2`) is unchanged. Historical CHANGELOG / README notes that record the pre-rename `FoolDev/Thanatos-27B` slug are left verbatim. - **Repointed the MoE-sibling cross-links** (README sibling badge, intro link, comparison-table header, "Related models" row, and `CITATION.cff` references entry) to `FoolDev/Janus-35B-HERETIC`, and set the git remote to `git@hf.co:FoolDev/Thanatos-27B-HERETIC`. ### Changed (base swap → llmfan46 Heretic v2) - **Swapped the immediate base from `Qwen/Qwen3.6-27B` (vanilla) → [`llmfan46/Qwen3.6-27B-uncensored-heretic-v2`](https://huggingface.co/llmfan46/Qwen3.6-27B-uncensored-heretic-v2)** — an uncensored "Heretic" abliteration of the same dense Qwen 3.6 27B arch. It is `qwen35`-stamped like vanilla Qwen 3.6 27B, so the Architecture section, the `config.json` discussion, and the bundled blob's loadability are all unchanged. **Base-lineage swap only:** the repo name `Thanatos-27B`, the `thanatos-27b` Ollama tag, the `FoolDev/Thanatos-27B` URL, and the Claude Fable 5 teacher slot are all kept — this is *not* the repo rename that accompanied the earlier (reverted) attempt. - README: frontmatter `base_model:` + `base_model_relation: finetune`; `heretic` / `uncensored` tags added (`library_name: transformers` kept). Base badge, tagline, fact line (`Type: Distilled + Abliterated`), sibling paragraph, bundled-blob callout, MTP section, Related-models table, and Credits reframed to the Heretic v2 lineage; `Qwen/Qwen3.6-27B` is still referenced as the *upstream pre-Heretic* base. - Tooling: `scripts/build.sh` `REPO_ID` → `llmfan46/Qwen3.6-27B-uncensored-heretic-v2-GGUF`, filename pattern `Qwen3.6-27B-uncensored-heretic-v2-${QUANT}.gguf`, and `Q3_K_S` → `Q3_K_M` for buildable-quant references (the Heretic GGUF repo publishes no Q3_K_S). `scripts/fetch_vision.sh` → BF16 `Qwen3.6-27B-mmproj-BF16.gguf` from that repo (unsloth's `mmproj-F16.gguf` kept as a documented fallback projector). `Modelfile`, `CITATION.cff`, and `examples/*` flipped to match; `check.sh` filename guard comment updated. Default `TAG` stays `thanatos-27b`. - `banner.svg` subtitle → "Qwen 3.6 Heretic · Dense 27B · Fable 5"; `banner.png` re-rasterized from the updated SVG (unchanged 766×154, 2×). - **Bundled `Thanatos-27B.Q4_K_M.gguf` rebundled** — replaced the legacy pre-Heretic Qwen quant with the Heretic v2 Q4_K_M from `llmfan46/Qwen3.6-27B-uncensored-heretic-v2-GGUF` (`qwen35`-stamped; verified against the README Architecture claims via `make verify-arch`). Pulling `hf.co/FoolDev/Thanatos-27B` now serves the uncensored Heretic v2 behavior directly, no local build required. ### Added (heretic abliteration tooling) - **`scripts/heretic_remote.sh` + `scripts/heretic_unattended.py`** — submit an unattended `heretic-llm` abliteration run to `hf jobs` (the local box has no GPU / not enough RAM for a 27B abliteration). `heretic_unattended.py` monkey-patches `heretic.main`'s interactive prompts to pick the first Pareto-front trial and push to HF headlessly (heretic-llm has no batch mode); pinned to a specific `heretic-llm` version by the caller. `heretic_remote.sh` base64-ships the driver into an `hf jobs run` (dry-run by default; `SUBMIT=1` to actually submit). Default upload target is `FoolDev/Thanatos-27B-Heretic-v2` — **not** the bare `-Heretic` slug, which 307-redirects onto this base repo and would clobber it. ### Changed (support badge → Tokyo Night banner) - **Replaced the shields.io "Buy me a coffee" badge** in the README badge row with a custom `bmc-banner.svg` support banner, themed to match `banner.svg` (same Tokyo Night gradient, monospace wordmark, warm `#e0af68` accent; animated rising steam, a pulsing hot-cup glow, a sweeping warm scan beam, a slow shimmer on "coffee", and a blinking cursor). Links to the unchanged `buymeacoffee.com/cardoffoolm`. Adds `bmc-banner.svg`; no other badge or link changed. Mirrors the same banner in `FoolDev/Janus-35B`. ### Fixed (post-revert / post-relabel drift cleanup) - **`.gitignore`** — stale `hf.co/FoolDev/Thanatos-27B-Heretic` slug in the "Use this model" comment corrected to `hf.co/FoolDev/Thanatos-27B`; the bulk rename-back missed this one comment. - **`Modelfile`** — dropped the orphaned `(Heretic v2 base is not benched here yet; rebundle pending.)` benchmark comment. The base was reverted to vanilla `Qwen/Qwen3.6-27B`, so the surrounding tok/s figures are already the current vanilla-base numbers and no Heretic rebundle is pending. - **`scripts/smoke_test.sh`** — the "model not found" build hint described `load_bundle.sh` as loading `this repo's qwen36 bundle`; the shipped bundle is `qwen35`-stamped (canonical since `e03e10e`), so the hint now says `qwen35`. - **`README.md`** — "Why a 27B variant?" quoted the dense 27B at Q3_K_S as `~10 tok/s`, the superseded ROCm snapshot. The Hardware-requirements reference section already lists the current Vulkan measurement (~12.3 tok/s) as the winner, so the intro now reads `~12 tok/s`. ### Changed (teacher label — Opus 4.7 → Fable 5) - **Reasoning-teacher references** relabeled from `Claude Opus 4.7` to `Claude Fable 5` across the model card (TL;DR, architecture line, Janus-35B sibling note, and Credits), `banner.svg` subtitle, and `CITATION.cff`. The external dataset slug `crownelius/Opus-4.7-Reasoning` in the metadata is left unchanged — it is a real Hugging Face dataset path, not a teacher label. Mirrors the same relabel in `FoolDev/Janus-35B`. ### Changed (project name reverted: Thanatos-27B-Heretic → Thanatos-27B) - **HF repo renamed back to `FoolDev/Thanatos-27B`** via the HF UI (HF serves a 307 redirect from `FoolDev/Thanatos-27B-Heretic` to the canonical name). Now that the base is also reverted to vanilla `Qwen/Qwen3.6-27B`, the `-Heretic` suffix had no remaining justification. - **Bulk-renamed `Thanatos-27B-Heretic` → `Thanatos-27B`** and `thanatos-27b-heretic` → `thanatos-27b` across README, Modelfile, scripts, examples, CITATION.cff, Makefile, .gitignore. CHANGELOG history entries below this one retain their original wording (they document actions taken under the Heretic name, before this revert). - **Banner wordmark** — dropped the `-HERETIC` tspan from banner.svg, leaving `THANATOS-27B`. banner.png re-rasterized at 2× via rsvg-convert. - **README "Note on the name" callout removed** — no longer applicable since name and base are aligned again. - **CITATION.cff abstract** — dropped the trailing parenthetical about the reverted Heretic swap. - **Local git remote re-pointed** from `git@hf.co:FoolDev/Thanatos-27B-Heretic` to `git@hf.co:FoolDev/Thanatos-27B`. ### Reverted (base swap to Heretic v2 — name kept, base back to vanilla Qwen) - **Undone the `Qwen/Qwen3.6-27B` → `llmfan46/Qwen3.6-27B-uncensored-heretic-v2` base swap** that shipped in `16e1ddd` and was polished in subsequent commits. Current base is back to vanilla `Qwen/Qwen3.6-27B`. README frontmatter `base_model:`, the `Base-…` badge, the Architecture line, the sibling paragraph, the Quick-start path C, the Local-apps table, the Vision section, the Related-models table, the Credits, and the Known-limitations section all flipped back to the pre-swap Qwen-only framing. `heretic` / `uncensored` tags removed (`imatrix` stays — the bundled blob is still iMatrix-quantized regardless of which base is described). `base_model_relation: finetune` removed; this is a packaging wrapper, not a finetune. - **Tooling flipped back to unsloth's GGUF mirror.** `scripts/build.sh` `REPO_ID` back to `unsloth/Qwen3.6-27B-GGUF` with filename pattern `Qwen3.6-27B-${QUANT}.gguf`; quant list back to the unsloth catalog (Q3_K_S restored as the smallest practical quant). `scripts/fetch_vision.sh` defaults back to `PRECISION=F16` and `mmproj-F16.gguf` from unsloth. Modelfile preamble flipped. `examples/transformers_quickstart.py` `MODEL_ID` back to `Qwen/Qwen3.6-27B`. `examples/README.md` and `examples/llama_cpp_vision.py` recipes flipped. `CITATION.cff` title, abstract, references, and keywords flipped. `Makefile` help-text + `build` docstring flipped. `.gitignore` comments + whitelist + rebadge-artifact glob flipped. - **`banner.svg`** subtitle reverted `Dense 27B · Opus 4.7 distilled · uncensored` → `Qwen 3.6 · Dense 27B · Opus 4.7 distilled`. `THANATOS-27B-HERETIC` wordmark **kept** — the project name string and HF repo URL are preserved per explicit choice ("undo base only, keep name"). `banner.png` re-rasterized at 2× via rsvg-convert. - **Project name string `Thanatos-27B-Heretic` and Ollama tag `thanatos-27b-heretic` retained** across all files. HF repo URL stays at `FoolDev/Thanatos-27B-Heretic`; git remote unchanged. A "Note on the name" callout added to the README tagline explaining the name-vs-base mismatch so users aren't surprised. - **Bundled blob unchanged** (`Thanatos-27B.Q4_K_M.gguf` LFS pointer SHA `5ed60d0a...`). It was always the legacy unsloth Qwen Q4_K_M quant; with the base reverted, the blob and the declared base are now consistent again. The "Bundled blob status" callout in TL;DR removed since it no longer applies. - **HF repo migration:** the HF repo at `FoolDev/Thanatos-27B-Heretic` keeps its current name (the user's earlier rename via HF UI stands). If you want to also rename the HF repo back to `FoolDev/Thanatos-27B`, that's a separate HF UI action — HF will serve a 307 redirect from the new name to the old once renamed. ### Changed (acknowledge HF's `imatrix` auto-tag in frontmatter) - **Added `imatrix` to the README `tags:` list.** HF's tag auto-detector was surfacing `imatrix` on the rendered model card (from the bundled Q4_K_M GGUF's metadata — the legacy unsloth-derived blob was iMatrix-calibrated) even though the frontmatter didn't declare it. Same "auto-tag surprise" pattern that motivated the earlier `general.tags` strip (`9cc78e7`) and the `config.json` drop (`5302d10`). Acknowledging it in the frontmatter makes the explicit tag list the source of truth instead of fighting the detector — the bundled blob is in fact iMatrix-quantized, so the tag is accurate. ### Changed (post-rename polish — banner, gitignore, Makefile) - **`banner.svg`** title updated `THANATOS-27B` → `THANATOS-27B-HERETIC` (added `-HERETIC` tspan in Tokyo Night red `#f7768e`, matching the abliteration vibe). Subtitle updated `Qwen 3.6 · Dense 27B · Opus 4.7 distilled` → `Dense 27B · Opus 4.7 distilled · uncensored`. (Initial post-rename subtitle was `Heretic v2 · Dense 27B · Opus 4.7 distilled · uncensored`, but rasterization showed it overflowed the 383px viewBox and truncated `uncensored` → `unc`. Dropped the `Heretic v2 ·` prefix since the `-HERETIC` wordmark already conveys the lineage; the v2 specifier lives in the README badges and frontmatter.) ViewBox unchanged (383×77). - **`banner.png`** re-rasterized from `banner.svg` at 2× density (766×154) via `rsvg-convert -w 766 -h 154`. Previous PNG was the pre-rename `THANATOS-27B` snapshot from 2026-05-16 at 1× (383×77, 9.8 KB); new PNG is 24 KB. Used as a fallback for tools that don't render SVG; the README itself loads `banner.svg` directly at `width="100%"`. - **`dense-flow.png`** added as a fallback sibling to `dense-flow.svg`, rasterized at 2× density (1600×480, 78 KB) via `rsvg-convert -w 1600 -h 480`. Captures a single static frame of the animated forward-pass visualization (the SVG shows all 64 layer-bars at baseline opacity with the left-to-right pulse glow at its starting position). README itself loads `dense-flow.svg` directly; PNG is for non-SVG renderers. File table entry added under "What's here". ### Fixed (Heretic v2 GGUF size quotes) - **README quant-size figures corrected against actual files** at `llmfan46/Qwen3.6-27B-uncensored-heretic-v2-GGUF` (HEAD requests against the LFS-redirected URLs): Q3_K_M actual 13.30 GB (was quoted `~12 GB` in two TL;DR-style spots); Q5_K_M actual 19.23 GB (was quoted `~20 GB` / `20 GB` in two places). Updates: `README.md` "For other quants" line and the `make build QUANT=Q5_K_M` comment in Quick start; `examples/README.md` Setup/Ollama section. Q4_K_M ~17 GB quote (actual 16.55 GB) rounds correctly and was left alone. The more detailed references in the architecture table, hardware table, and `make build QUANT=Q3_K_M` comment already used `~13 GB` for Q3_K_M; this aligns the rest of the doc with those. - **`.gitignore`** comments refreshed to mention llmfan46 Heretic v2 GGUF source and the canonical `hf.co/FoolDev/Thanatos-27B-Heretic` Ollama URL. Whitelist extended with `!Thanatos-27B-Heretic.*.gguf` and rebadge-experiment block extended with `Thanatos-27B-Heretic.*.qwen[0-9]*.gguf` so the pending Heretic rebundle is already accommodated. - **`Makefile`** help-text and `build` docstring updated to reference the Heretic v2 GGUF source (`llmfan46/...`) and use `Q3_K_M` in examples (no `Q3_K_S` in the Heretic repo). `clean` target now also removes legacy `Thanatos-27B.*.qwen[0-9]*.gguf` rebadge artifacts (the `Thanatos-27B.Q4_K_M.qwen35.gguf` / `Thanatos-27B.Q4_K_M.qwen36.gguf` files from the pre-rename stamp saga that still sit in working trees). ### Changed (project rename + base swap to Heretic v2) - **Renamed project `Thanatos-27B` → `Thanatos-27B-Heretic`** and **swapped immediate base from `Qwen/Qwen3.6-27B` (vanilla) → `llmfan46/Qwen3.6-27B-uncensored-heretic-v2`** (an uncensored Heretic-style abliteration of the dense Qwen 3.6 27B base). README, Modelfile preamble, `CITATION.cff`, all scripts, and all examples now refer to `Thanatos-27B-Heretic` / `thanatos-27b-heretic` (lowercase Ollama tag) and pull GGUFs from `llmfan46/Qwen3.6-27B-uncensored-heretic-v2-GGUF`. Architecture is unchanged (still Qwen 3.6 dense 27B, `qwen35`-stamped, hybrid SSM+attention stack) — only the weights' finetune lineage moves. (Initial docs commit `16e1ddd` used the word order `Thanatos-Heretic-27B`; corrected here to match the actual HF repo name `FoolDev/Thanatos-27B-Heretic` the user created via the HF rename UI.) - **`base_model:` frontmatter** flipped to `llmfan46/Qwen3.6-27B-uncensored-heretic-v2`; `base_model_relation: finetune` added; `heretic` and `uncensored` tags appended. `library_name: transformers` stays for HF Hub placement (snippet trap accepted as before; `config.json` is still intentionally absent). - **`scripts/build.sh`** now points `REPO_ID` at `llmfan46/Qwen3.6-27B-uncensored-heretic-v2-GGUF` and uses the filename pattern `Qwen3.6-27B-uncensored-heretic-v2-${QUANT}.gguf`. Default `TAG` is `thanatos-27b-heretic`. Note: no `Q3_K_S` in the Heretic GGUF repo — use `Q3_K_M` for the smallest practical quant (`Modelfile` preamble and README hardware/quick-start tables updated accordingly). - **`scripts/fetch_vision.sh`** defaults flipped to `PRECISION=BF16` and `REPO_ID=llmfan46/Qwen3.6-27B-uncensored-heretic-v2-GGUF` (`Qwen3.6-27B-mmproj-BF16.gguf`, ~931 MB). Unsloth's `mmproj-F16.gguf` is documented as a reference fallback for users who want the F16/F32 variants. - **Bundled blob status:** the in-repo `Thanatos-27B.Q4_K_M.gguf` LFS pointer is unchanged — still the legacy pre-Heretic Qwen 3.6 27B Q4_K_M quant (`5ed60d0af4650a854b1755bd392f9aef4872643dc25a254bc68043fa638392a0`). Behaves identically to vanilla Qwen 3.6 27B for now. Heretic v2 re-quantization + rebundle (file rename to `Thanatos-27B-Heretic.Q4_K_M.gguf` + LFS swap) is a separate follow-up; users wanting actual Heretic behavior today should use the local-build path (`make build`). - **HF repo migration:** the HF repo was renamed by the user from `FoolDev/Thanatos-27B` to `FoolDev/Thanatos-27B-Heretic` (HF serves a 307 redirect from the old path). Local git remote re-pointed from `git@hf.co:FoolDev/Thanatos-27B` to `git@hf.co:FoolDev/Thanatos-27B-Heretic` to follow. - **CHANGELOG history left intact:** entries below this one still reference `Thanatos-27B` and the bundled-blob saga as they happened on the old repo identity. Historical, not retconned. ### Changed (HF tag-surface cleanup — `general.tags` strip + `config.json` drop) - **Stripped `general.tags` KV from the bundled GGUF** (`9cc78e7`, 2026-05-20). Drops the upstream-baked `unsloth` and `image-text-to-text` tags that `llama.cpp`'s converter copies into GGUFs from `unsloth/Qwen3.6-27B-GGUF`; both surfaced on the HF model page and obscured this card's positioning. Tensors byte-identical; only the `general.tags` KV is gone. - **Dropped `config.json`** (`5302d10`, 2026-05-20) to suppress HF's tag auto-detector surfacing `qwen3_5` in the repo header — the detector reads `architectures` from `config.json`. Consequence: `AutoModelForCausalLM.from_pretrained( "FoolDev/Thanatos-27B")` no longer works on its own. `examples/transformers_quickstart.py` and the README transformers note now point users at upstream `Qwen/Qwen3.6-27B` directly (tensors byte-identical, so the result is the same model). `library_name: transformers` stays in the model-card metadata for Hub placement. ### Reverted (safetensors mirror experiment) - **Mirrored Qwen/Qwen3.6-27B's safetensors set into this repo (`b420378`, 2026-05-20), reverted within the day** (`50f6684` + `9cf363e`, 2026-05-21). 15 sharded `.safetensors` + tokenizer + processor configs (~58 GB) were briefly added so users wanting GGUF + safetensors in one place could skip a second `hf download`; reverted on reflection. Transformers users continue to pull from upstream `Qwen/Qwen3.6-27B`. `.gitignore` whitelist for the Qwen sharded naming pattern (`0c5bee4`) was removed alongside the mirror; `*.safetensors` block rule is back to baseline. ### Changed (5th round trip — qwen36 → qwen35, retested next-day) - **Bundle re-stamped `general.architecture: 'qwen36'` → `'qwen35'`** in `hf upload` commit `e03e10e` (HF), 2026-05-20 midday — 8 minutes after a brief re-flip to qwen36 (`ae67ed1`) at the start of a fresh next-day session. Tested the friction once more in a new session; conclusion unchanged from the 4th round trip. Bundle blob SHA back to `5ed60d0af4650a854b1755bd392f9aef4872643dc25a254bc68043fa638392a0` (byte-identical tensors across all qwen35 stamps). - README "Architecture > History" intro count goes from six flips to eight; bullets gain 7th (`ae67ed1`) and 8th (`e03e10e`) entries; the "current state" marker moves to `e03e10e`. README TL;DR heal-hf note, Modelfile preamble, `scripts/load_bundle.sh`, `scripts/heal_hf_pull.sh`, and `examples/README.md` updated to reference `e03e10e` as the latest qwen35 re-stamp. ### Changed (4th round trip — qwen36 → qwen35, friction concretely tested) - **Bundle re-stamped `general.architecture: 'qwen36'` → `'qwen35'`** in `hf upload` commit `978798f` (HF). Fourth stamp flip on the 2026-05-19 session, this one resolving the saga on the upstream-canonical `qwen35` side after the 3rd round trip (`973d7ef`) had its friction empirically tested. Seven sequential fresh-pull → heal-hf cycles on Strix Halo tonight made the friction concretely-experienced rather than hypothetical: every cycle worked (the heal flow is solid and deterministic), every cycle was an unnecessary obstacle for users who just want `ollama run` to work first try. The audit (`a4d3b6e`) called the canonical stamp correctly; the practical friction outweighed the version-specificity payoff. New bundle blob SHA is `5ed60d0af4650a854b1755bd392f9aef4872643dc25a254bc68043fa638392a0` (same as the post-2nd-round-trip `72259c1` and post-1st-round-trip `964e418` qwen35 blobs — byte-identical tensor data across all qwen35 stamps). - README "Architecture" section collapsed back to the qwen35- canonical narrative: the three-layer evidence (Qwen's HF configs, llama.cpp's converter, llama.cpp's model code) stays intact; the "Stamp choice" subsection (which framed qwen36 as a deliberate project preference) is removed; History picks up a 6th-flip bullet documenting the `978798f` revert and what finally made the friction tradeoff decisive. - README "Heads up" callout removed from TL;DR; one-liner `ollama run hf.co/FoolDev/Thanatos-27B` restored as the canonical path. The legacy heal-hf recovery note stays (for users with qwen36-stamped blobs from either of the two qwen36 windows still in their store). - README "Local apps" intro + Ollama / llama.cpp table cells flipped back to "loads directly". The llama.cpp recipe is back to `hf download FoolDev/Thanatos-27B Thanatos-27B.Q4_K_M.gguf --local-dir . && llama-server -m Thanatos-27B.Q4_K_M.gguf` — the qwen35 stamp makes the bundled file directly loadable, no rebadge step needed. - `Modelfile`, `scripts/load_bundle.sh`, `scripts/heal_hf_pull.sh`, `examples/README.md`, and `Makefile` preambles / help text / examples README intros all flipped back to describing the qwen35 stamp as current state. The rebadge code paths in load_bundle.sh and heal_hf_pull.sh stay — both have explicit legacy-recovery branches that no-op on qwen35-stamped bundles but rebadge on encountering a qwen36-stamped one (pre-`964e418` v0.6.0-era pulls, or pulls in the 3rd-round-trip window between `973d7ef` and `978798f`). - `scripts/rename_arch.py` example in the README's Rebadge utility subsection flipped back to qwen36 → qwen35 (the actually-useful direction now — qwen35 → qwen36 was only ever needed to produce the now-reverted bundles). ### Note on the saga - Eight total stamp flips across 2026-05-19/20: qwen35 → qwen36 (v0.6.0) → qwen35 (`964e418`) → qwen36 (`07fa120`) → qwen35 (`72259c1`) → qwen36 (`973d7ef`) → qwen35 (`978798f`) → qwen36 (`ae67ed1`) → qwen35 (`e03e10e`). The repo is now firmly on the upstream-canonical `qwen35` stamp; the audit evidence (under "Architecture" in the README) is the durable artifact of *why*. The history bullets document the path. If a future maintainer wants to test the version-specific stamping again, the rebadge tools (`make load-bundle`, `make heal-hf`, `scripts/rename_arch.py`) are all still wired up to do it — the lesson is in the friction, not in the absence of tooling. ### Validated (2026-05-19 evening, post-`973d7ef`) Empirical confirmations that the qwen36 stamp + heal-hf recovery is operationally solid, captured here so they're not just lost to chat scrollback: - **Heal-hf reproducibility — 5 cycles.** Ran the full `ollama rm` → fresh `ollama run` (qwen36 500) → `make heal-hf` loop five times in sequence on this box (ROG Flow Z13, Strix Halo). Every cycle produced the same healed blob digest `sha256:5ed60d0af4650a854b1755bd392f9aef4872643dc25a254bc68043fa638392a0` (byte-identical to the pre-2nd-round-trip qwen35 stamp). The rebadge script (`scripts/heal_hf_pull.sh` via `make heal-hf`) is deterministic across pulls, and the qwen35 blob it produces matches the upstream-canonical stamp. The friction is bounded: one error message per fresh pull, one command to fix, no follow-on issues. - **Tool calling end-to-end on the healed tag.** Ran `MODEL=hf.co/FoolDev/Thanatos-27B:Q4_K_M make smoke-tools` after the 1st heal cycle. Server reachable, model present, capabilities included `tools` (HF Ollama bridge wired `.Tools` / `.ToolCalls` from `template`), plain round-trip returned `OK`, tool-call round-trip returned a structured `tool_calls` array with `name=get_weather`, `city=Tokyo`. Confirms the bridge files (`template` / `system` / `params`) are not collateral damage from the rebadge — Ollama's capability detector still finds the right hooks. - **Architecture metadata still matches README claims.** Ran `make verify-arch` against both `Thanatos-27B.Q4_K_M.qwen35.gguf` (the source) and `Thanatos-27B.Q4_K_M.qwen36.gguf` (the current HF bundle); all 13 quantitative bullets verified clean on each stamp. No silent metadata drift across the `scripts/rename_arch.py` flip. - **Performance unchanged across stamps.** Q4_K_M Vulkan bench on the healed qwen36 tag came in at 9.32 tok/s aggregate (run 3, commit `693cf65`), within 0.1% of run 1's 9.31. The metadata-only rebadge produces an inference-path-equivalent model. ### Added - `scripts/verify_arch.py` + `make verify-arch`. After correcting the MTP bullet (see Fixed below) it was worth confirming the remaining Architecture forward-pass bullets weren't carrying any other quiet inaccuracies. Cross-checked all thirteen quantitative claims (layer count, hidden / FFN dims, head counts, head_dim, RoPE factor, hybrid stack interval, SSM dims, vocab, context) against the actual GGUF metadata keys; every one verified clean on both the qwen35- and qwen36-stamped bundles. The script captures that mapping as a reusable artifact: pass it a path to a Thanatos-27B (or compatible) GGUF and it prints each bullet alongside the metadata key it derives from, exiting non-zero on mismatch. Useful after future re-stamps / re-conversions / upstream converter updates. Not folded into `make check` because it has to read the 17 GB tensor file (LFS smudge required) — run on demand. ### Fixed - README "Multi-token prediction (MTP)" bullet corrected. The earlier wording — "MTP head trained for speculative decoding" — was technically true about the upstream `Qwen/Qwen3.6-27B` safetensors but misleading for the GGUF bundle this repo ships and for llama.cpp / Ollama users in general: - **GGUFs are stripped.** `convert_hf_to_gguf.py` explicitly skips MTP tensors for the `qwen35` / `qwen35moe` arch family ("MTP tensors are not used at inference yet; align with Qwen3Next behaviour"). Confirmed empirically via `gguf.GGUFReader` on both `Thanatos-27B.Q4_K_M.qwen35.gguf` and the source `unsloth/Qwen3.6-27B-GGUF` Q4_K_M: both have 851 tensors and zero entries matching `mtp.*` / `draft.*` / `eagle.*` / `spec.*`. Last tensor in either is `blk.63.post_attention_norm.weight` — the final layer norm, no MTP head after it. - **Loader doesn't support it.** `src/models/qwen35.cpp` and `qwen35moe.cpp` contain no MTP / nextn references; even if the tensors were in the GGUF, the loader wouldn't use them. - **PR #22673's scope.** llama.cpp's MTP support (merged 2026-05-16) was added for other architectures, not the `qwen35` family. The README bullet now says so explicitly, points to vLLM (`qwen3_next_mtp`) and SGLang (`--speculative-algo NEXTN`) as the working consumer recipes against the safetensors, and notes that we're tracking the follow-up that would extend MTP to qwen35 / qwen35moe. ### Changed (3rd round trip — qwen35 → qwen36, user-directed despite audit) - **Bundle re-stamped `general.architecture: 'qwen35'` → `'qwen36'`** in `hf upload` commit `973d7ef` (HF). Third stamp flip on the 2026-05-19 session, undertaken at project-owner direction after the upstream-evidence audit (`a4d3b6e`) had already shipped. The audit conclusion stands — `qwen35` is the upstream-canonical label for the whole Qwen 3.5 / 3.6 hybrid family, with concrete code-level evidence (Qwen's own `Qwen3_5*ForConditionalGeneration` HF classes, llama.cpp's converter mapping, the explicit Qwen 3.6 type branches in `qwen35.cpp` / `qwen35moe.cpp`). The project owner heard the evidence and chose the version-specific qwen36 stamp anyway, accepting the `make heal-hf` friction this creates on every fresh `ollama run hf.co/FoolDev/Thanatos-27B`. New bundle blob SHA is `b2b63b941d714b0aff5e9afc9b337e7607b84e371ba991c182d92d0f7805c0e9`; tensor data is byte-identical to the prior qwen35-stamped blob (`5ed60d0a…`), only the `general.architecture` KV (and namespaced KV keys) flipped. - README "Architecture" section restructured into three subsections: - **Stamp choice** — leads with the bundle's qwen36 declaration, states the tradeoff (loader friction in exchange for version-specific labeling) up front. - **Upstream convention — why `qwen35` would also be valid** — keeps the three-source audit evidence intact (Qwen HF configs, llama.cpp converter, llama.cpp model code) and explicitly says a future maintainer can flip back via `make load-bundle` / `make heal-hf`. - **History** — logs all three flips on the 2026-05-19 session with commit refs and rationale per flip; the new entry annotates the choice as project-owner preference post-audit. - README "Heads up" callout re-added to TL;DR (`ollama run hf.co/...` 500s on first inference; `make heal-hf` rebadges the cached blob). TL;DR ordering: heads-up callout → two-step pull-and-heal path → optional unsloth-based alternative for users who don't want to heal. - README "What's here" intro paragraph, "Local apps" intro, and Ollama table cell restored to the qwen36 / heal-hf-needed narrative. - `Modelfile`, `scripts/load_bundle.sh`, and `scripts/heal_hf_pull.sh` preambles flipped back to describing the qwen36 bundle as the current state and the rebadge as the primary use case (rather than legacy recovery). - `Makefile` `load-bundle` help text + example comment restored to the "qwen36-stamped bundle, rebadged to qwen35" phrasing. ### Note - The audit shipped in `a4d3b6e` was not reverted. Its evidence remains accurate: `qwen35` *is* the upstream-canonical family label. The current stamp is a deliberate departure from that convention, not a refutation of it. A maintainer who later decides the friction isn't worth it can flip the bundle back to qwen35 with a single `make load-bundle` invocation; the audit evidence already explains why that flip would be valid. ### Changed (upstream-evidence audit — qwen35 is canonical, not a workaround) in commit 964e418 (HF-direct `hf upload`). Reverses the v0.6.0 `qwen36` stamp from e1f78fa, which itself flipped qwen35 → qwen36 deliberately. The proximate trigger: today's subagent recheck found no PR or tracking issue for a `qwen36` arch entry in either `ggml-org/llama.cpp` or `ollama/ollama`, and the actual Qwen 3.6 family upstream already loads under `qwen35moe` — so the qwen36 stamp was going to stay ahead-of-upstream indefinitely, not for a release or two. Day-to-day cost (every fresh `ollama run hf.co/FoolDev/Thanatos-27B` hitting a 500, every user needing `make heal-hf` to recover) outweighed the architectural-honesty benefit. Tensor data stays byte-identical; only the `general.architecture` KV (and the namespaced KV keys) flips. New bundle blob SHA is `5ed60d0af4650a854b1755bd392f9aef4872643dc25a254bc68043fa638392a0` — the same hash `make load-bundle` and `make heal-hf` have been producing locally this whole session. - README "Heads up" callout (qwen36 load-failure warning) removed. TL;DR collapsed back from "three paths around it" to a single one-liner: `ollama run hf.co/FoolDev/Thanatos-27B` now loads on stock Ollama. `make heal-hf` is mentioned as the recovery path for anyone who pulled the v0.6.0 bundle and still has the qwen36 blob in their store. - README "Architecture" section, Quick start option A, `examples/README.md` Ollama setup, and `Modelfile` preamble all collapsed back to the pre-qwen36 single-path narrative. The long "qwen36 is unloadable / here are the workarounds" prose was replaced with a one-paragraph history note pointing at commit 964e418 + the [CHANGELOG](CHANGELOG.md) entry. Net -39 lines across the three doc surfaces. `scripts/rename_arch.py` example in Architecture flipped from qwen36 → qwen35 to qwen35 → qwen36 to match its new "tool kept around for future flips" framing. - **Round trip (2026-05-19 evening).** The bundle briefly flipped back to `qwen36` in commit `07fa120` plus the doc re-expansion in `e9c7366`, on the theory that architectural-honesty was worth the heal-hf friction. A fresh-pull integration test on the Strix Halo box reproduced the friction end-to-end (`ollama run` 500 on first pull, recover via `make heal-hf`, smoke-test then passed) — and the friction was worse in practice than the doc prose suggested. Reverted to qwen35 in `hf upload` commit `72259c1` + the present commit (revert of `e9c7366`). Net bundle state: same `5ed60d0a...` blob as before the round trip; no user-facing impact unless a user happened to pull in the ~1 hour window when the bundle was qwen36-stamped (for them, one `make heal-hf` is the recovery). ### Note - `make heal-hf`, `make load-bundle`, and `scripts/rename_arch.py` stay in the repo and stay useful. heal-hf is idempotent on qwen35 (skips early); load-bundle does a no-op rebadge on a qwen35 bundle and runs `ollama create` as normal. ### Changed (upstream-evidence audit — qwen35 is canonical, not a workaround) - Read the actual upstream code paths for Qwen 3.6 loading and found that `qwen35` isn't a stand-in awaiting `qwen36` support; it's the upstream-canonical name for the whole Qwen 3.5 / 3.6 hybrid SSM + attention family. Three converging pieces of evidence, now folded into the README "Architecture" section: - **Qwen's own HF configs** for both `Qwen/Qwen3.6-27B` and `Qwen/Qwen3.6-35B-A3B` declare `model_type: "qwen3_5"` / `"qwen3_5_moe"` and the `Qwen3_5*ForConditionalGeneration` arch classes. No `Qwen3_6` arch class exists in `transformers` — Qwen reuses the 3.5 class names for 3.6 weights. - **`convert_hf_to_gguf.py`** in `ggml-org/llama.cpp` registers `Qwen3_5ForCausalLM` → `MODEL_ARCH.QWEN35` and `Qwen3_5MoeForCausalLM` → `MODEL_ARCH.QWEN35MOE`, so the unsloth GGUFs this repo pulls from are natively qwen35-stamped out of the converter. - **`src/models/qwen35.cpp`** has an explicit `case 64: type = LLM_TYPE_27B` branch (this model); **`qwen35moe.cpp`** has `case 40: type = LLM_TYPE_35B_A3B` (the Janus-35B sibling base). The arch entries were written to load Qwen 3.6 weights, not just Qwen 3.5; this is code-level confirmation, not interpretation. - Reframed the README "Architecture" section accordingly: the three pieces of evidence are listed inline; the "qwen35 is the canonical load-time label" claim is now grounded rather than asserted; the History paragraph softens the v0.6.0 stamp choice from "architecturally-honest" to "version-specific on the theory that `qwen35` was a loader stand-in awaiting `qwen36` support" — a theory the audit shows was mistaken. - Flipped the `scripts/rename_arch.py` example back to qwen36 → qwen35 (the direction that actually serves the only realistic use case: legacy v0.6.0-era recovery). Previous direction (qwen35 → qwen36) made less sense given the audit conclusion. - Tooling unchanged. `scripts/rename_arch.py`, `make heal-hf`, `make load-bundle` still exist and still work; they're just documented honestly as legacy recovery + generic flip utility rather than as workarounds awaiting upstream changes that will never come. ### Changed (round-trip residue cleanup) - Five doc surfaces still carried the v0.6.0-era "bundle is qwen36-stamped, fails today, rebadge to qwen35 first" narrative that the earlier `83022eb` / `4811e8d` collapses missed. Reframed to describe the current qwen35-stamped reality: - **README** "What's here" intro paragraph (the secondary TL;DR-style block above Architecture): flipped the "qwen36-stamped → fails today, see Heads-up above" example to the same one-liner the primary TL;DR uses; dropped the "as a working-today fallback while the bundle waits on upstream qwen36 support" framing from the Q3_K_S paragraph since there's no longer anything to wait on. - **README** "Local apps" intro paragraph: removed the entire "every row below assumes you've rebadged it to qwen35" caveat; trimmed "or to bypass the qwen36 block today" from the Ollama table cell. - **README** "What's here" table cells for `load_bundle.sh` and `heal_hf_pull.sh`: reframed each as primary qwen35 path + legacy qwen36 recovery branch, instead of describing the rebadge as mandatory. - **`scripts/load_bundle.sh`** preamble: rewritten to describe the current qwen35-stamped bundle as the default; the qwen36 rebadge branch is documented as a fallback for legacy pre-`964e418` checkouts. - **`scripts/heal_hf_pull.sh`** preamble: reframed as recovery for pre-`964e418` v0.6.0-era pulls; clarified that fresh pulls don't need it. - **Makefile** `load-bundle` help text + example comment: dropped the "qwen36-stamped bundle, rebadged to qwen35" framing. Net -15 lines. Upstream re-poll (`gh search` against `ggml-org/llama.cpp` + `ollama/ollama` for `qwen36` PRs/issues, 2026-05-19): no qwen36 arch entry, no open PR or tracking issue — same answer as the earlier subagent recheck; the `qwen35` stamp choice still stands. (Side-finding worth noting for a future README update: llama.cpp PR `#22673` merged 2026-05-16 adds MTP speculative decoding for Qwen3.6, so the Architecture section's "MTP head trained for speculative decoding" claim now has a usable consumer path — out of scope for this cleanup.) ### Added - `scripts/check.sh` now greps for the `VAR="$(cmd 2>/dev/null | filter)"` silent-exit pattern that bit `heal_hf_pull.sh` (commit 385ed94). Pattern fires on any top-level assignment whose `$(...)` body both suppresses stderr (`2>/dev/null`) and pipes through a filter — the exact combination where `set -e + pipefail` swallows the failure of the first command and aborts the script before any explicit `[[ -z "${VAR}" ]]` check can run, leaving the user staring at `make: *** [Makefile:N: ] Error 1` with no diagnostic. Suggested fix in the failure message: `if VAR=$(cmd 2>/dev/null); then VAR2=$(filter <<<"${VAR}"); fi`. Currently passes (the heal-hf fix already uses the if-form); pre-commit hook + `make check` now block regressions. ### Fixed - `scripts/heal_hf_pull.sh` silent-exit when the target tag isn't pulled. The blob-locator line was `MODEL_BLOB="$(ollama show --modelfile "${TAG}" 2>/dev/null | awk ...)"`; under `set -e + pipefail`, an unpulled tag makes `ollama show` exit non-zero, pipefail propagates the non-zero through the pipeline, and set -e terminates the script before the explicit `[[ -z "${MODEL_BLOB}" ]]` check ever runs. The user saw only `make: *** [Makefile:47: heal-hf] Error 1` with no clue what was wrong. Refactored to use an `if MODELFILE_OUT=...; then` form, which takes the false branch on failure without tripping set -e. The "tag not pulled?" hint now fires as intended. ### Added - `scripts/heal_hf_pull.sh` now validates the rewritten manifest by running `ollama show ` immediately after the atomic mv into place, with rollback-on-failure semantics. Before this, a buggy rewrite that produced jq-parseable but ollama-rejected JSON (e.g. layer order off, missing required mediaType, digest that no longer matches the on-disk blob bytes) would only be discovered at the user's `ollama run` step — and by then the old qwen36 blob had already been removed, leaving the tag in a half-broken state with no easy recovery. Now: the script `cp`s the original manifest to a `.heal-backup` sidecar before the rewrite, runs `ollama show` against the rewritten manifest, and on failure `mv`s the backup back into place and exits non-zero. The old qwen36 blob cleanup (step 7, was step 6) only runs after validation passes, so a rollback always lands on a consistent state (original manifest + original blob still present). Backup is deleted on success. ## [0.6.0] - 2026-05-19 — `c336f44` Headline changes since 0.5.0 (over a two-week burst on 2026-05-19): - **Project renamed `Janus-27B` → `Thanatos-27B`** (HF repo, GGUF filenames, default Ollama tag, system-prompt identity, banner wordmark). MoE 35B sibling `FoolDev/Janus-35B` keeps the Janus name; the dense 27B splits off as its own identity. - **Bundle GGUF re-stamped `general.architecture: 'qwen35'` → `'qwen36'`** — the architecturally-honest label. Breaks `ollama run hf.co/FoolDev/Thanatos-27B` and direct `llama-server -m Thanatos-27B.Q4_K_M.gguf` on every stock loader until upstream adds the arch entry (which, as of 2026-05-19, has no PR or tracking issue in either ggml-org/llama.cpp or ollama/ollama). - **`make load-bundle`** — one-shot bundle → loadable local Ollama tag (LFS smudge + qwen36 → qwen35 rebadge + `ollama create`). - **`make heal-hf`** — heal an already-pulled HF-bridge tag in store by rebadging its model blob qwen36 → qwen35 and rewriting the manifest's model-layer digest, so the same `hf.co/...` tag becomes loadable in place without switching tag names. - **`scripts/rename_arch.py`** — generic GGUF arch renamer (metadata only, tensor data byte-identical). Used by load-bundle and heal-hf; reusable for future arch renames. - **Tool-calling** end-to-end via Ollama (Modelfile TEMPLATE wires `.Tools` / `.ToolCalls` so `ollama show` reports the `tools` capability and `/api/chat` / `/v1/chat/completions` accept tools arrays). HF Ollama bridge gets parallel coverage via root-level `template` / `system` / `params` files (HF's bridge does **not** read `Modelfile`); kept in sync via `scripts/check_bridge_sync.py`, wired into `make check`. - **`scripts/bench.sh` + `make bench`** — 3-prompt tok/s benchmark using Ollama's `eval_count` / `eval_duration`. Three measured data points logged on the Strix Halo reference hardware (Ryzen AI Max+ 395 / Radeon 8060S iGPU): Q3_K_S Vulkan 12.31 tok/s, Q4_K_M Vulkan 9.31 + 9.19 tok/s (two runs), Q3_K_S ROCm 10.14 tok/s (older backend snapshot). - **`scripts/smoke_test.sh` + `make smoke` / `make smoke-tools`** — server reachable, model present, tools capability, round-trip, token-leakage guard, opt-in tool-call round-trip (`TOOLS_TEST=1`). - **Vision section** restructured around llama.cpp / llama-cpp-python (Ollama vision is broken upstream — `qwen35` / `qwen35moe` arch entries present in Go engine, missing from the C++ fallback the Ollama runner switches to when mmproj is attached; [ollama/ollama#15898](https://github.com/ollama/ollama/issues/15898)). `llama-server --mmproj` HTTP path now the lead recommendation. - **Bundled GGUF**: `Thanatos-27B.Q4_K_M.gguf` (~17 GB) is the only redistributed quant, picked by HF's Ollama bridge for the default `:latest` tag. Other quants (Q3_K_S, Q5_K_M, Q6_K, safetensors) pulled from upstream on demand via `make build QUANT=...`. Bug-fix highlights: - `scripts/fetch_mmproj.sh` renamed to `scripts/fetch_vision.sh` — HF's Ollama bridge was filename-pattern-matching `mmproj*` anywhere in the repo and shipping the 2-KB bash script as the `application/vnd.ollama.image.projector` layer, breaking `ollama show` / `ollama run` with `Error: invalid file magic`. - `Modelfile` PARAMETER stop directives for `<|im_end|>`, `<|endoftext|>`, `<|im_start|>` — Ollama was only picking up `<|im_end|>` from GGUF metadata, so the model kept generating past `<|endoftext|>` and synthesised a fake user turn. - `examples/ollama_chat.py` thinking-trace handling under Ollama 0.24 (now reads `message.thinking`, falls back to `` extraction for older builds). See the full per-bullet changelog below. ### Added - `scripts/heal_hf_pull.sh` + `make heal-hf`: heal an already-pulled `hf.co/FoolDev/Thanatos-27B:...` tag in-store by rebadging its model blob (qwen36 → qwen35, metadata-only, byte-identical tensors) and rewriting the manifest's model-layer digest. Covers the user pain when `ollama run hf.co/FoolDev/Thanatos-27B` is typed from muscle memory, fails with the qwen36 500, and leaves ~17 GB of unloadable blob sitting in the store; before this, the only recovery was `ollama rm ` + switching to the separate `thanatos-27b` tag that `make load-bundle` builds. `make heal-hf` makes the same `hf.co/...` tag loadable in place. Idempotent (tags already on qwen35 / qwen35moe are skipped); content-addressed dedup means if the rebadged blob already exists in the store (e.g. from a prior `make load-bundle` run) the heal reuses it instead of double-allocating ~17 GB. Removes the old qwen36 blob if no other manifest references it. Stages the rebadge in `.cache/` rather than `/tmp` so the ~17 GB write doesn't blow past tmpfs (`mv` into `blobs/` stays an atomic same-filesystem rename on a normal Linux home-dir layout). - README TL;DR Ollama section now lists **three** paths: heal an already-pulled HF tag (`make heal-hf`), build from the bundle (`make load-bundle`), or bypass the bundle entirely (`make build`). New `scripts/heal_hf_pull.sh` entry added to the "What's here" table. ### Added - Second Q4_K_M Vulkan bench data point on the Strix Halo reference hardware (same machine + config as the existing 9.31 tok/s entry): **9.19 tok/s aggregate** (6210 tokens / 675.6 s; 9.40 / 9.29 / 9.16 short/medium/long). Measured 2026-05-19 against the qwen36-stamped HF-bridge tag after `make heal-hf` rebadged it to qwen35 in store — confirms the in-place heal produces a model with the same performance profile as `make load-bundle`. Aggregate is 1.3% below the existing run-1 (9.31), well inside the ±20% noise band the README warns about. Modelfile hardware notes updated. ### Changed - `examples/README.md` Ollama setup section: the previous "easiest path" told users to `ollama pull hf.co/FoolDev/Thanatos-27B` then run `ollama_chat.py` against the pulled tag — which fails today because the bundle is qwen36-stamped (the pull succeeds; the first inference call returns the qwen36 500). Section rewritten to lead with the pull + `make heal-hf` recovery (one extra command after pull, rebadges the blob in place so the same `hf.co/...` tag loads), and adds the `make load-bundle` / `make build QUANT=...` alternatives as the bypass-the-bundle paths that don't touch the HF pull at all. All three set MODEL explicitly so the right tag is used. - README "Architecture" section + Quick start option A: - Architecture body now notes that neither `ggml-org/llama.cpp` nor `ollama/ollama` has an open PR or tracking issue for a `qwen36` arch string (rechecked 2026-05-19), and that the actual Qwen 3.6-family upstream weights already load under `qwen35moe` — so the bundle's qwen36 stamp may stay ahead-of-upstream indefinitely, not just for a few releases. "Once upstream adds qwen36..." softened to "If upstream ever adds qwen36...", and the list of things deletable on that day grew from just `make load-bundle` to `make load-bundle` + `make heal-hf` + the whole workaround. - "The one-liner" subsection became two one-liners — one for the fresh-clone case (`make load-bundle`), one for the already-pulled case (`make heal-hf`). Mirrors the TL;DR. - Quick start option A's comment now points at `make heal-hf` as the in-place recovery for users who've already typed the direct `ollama run hf.co/...` form and hit the 500. Same block now lists both the failing one-liner and the heal + re-run recovery, so a skim-reader sees both without having to cross-reference the Architecture section. - `scripts/load_bundle.sh` + `make load-bundle`: one-shot path from the qwen36-stamped bundle → loadable Ollama tag. Handles the LFS smudge (`hf download FoolDev/Thanatos-27B Thanatos-27B.Q4_K_M.gguf` if the local copy is a pointer), inspects `general.architecture`, runs `scripts/rename_arch.py --from-arch qwen36 --to-arch qwen35` if needed (idempotent — reuses an existing qwen35-rebadged file with correct arch, regenerates if mismatched), then patches the `Modelfile` FROM and runs `ollama create thanatos-27b`. Reduces the manual rebadge recipe in README "Architecture" from four copy-pasted commands to one `make` target. Tag overridable via `TAG=…`; bundle path via `BUNDLE=…`. - README Quick start now describes **three** Ollama paths (was "two"): direct HF pull (fails today on qwen36), `make load-bundle` (loads this repo's bundle today via the rebadge), `make build` (bypasses bundle, pulls qwen35 from unsloth). Heads-up callout near the top of the model card now points at the one-liner. New `scripts/load_bundle.sh` entry added to the "What's here" table. - Makefile `clean` target now also removes `Thanatos-27B.*.qwen[0-9]*.gguf` rebadges (already gitignored, but `make clean` should sweep them too). ### Fixed - `scripts/smoke_test.sh` now matches the model tag case-insensitively against `/api/tags`. Ollama 0.24 normalizes lookups at query time but preserves whatever case was first registered on disk, so `make load-bundle` (TAG=thanatos-27b) can produce a tag listed as `Thanatos-27B:latest` if an earlier session left a `Thanatos-27B` manifest dir behind. The case-sensitive `startswith($m)` in the prior smoke check rejected the tag even though `ollama run thanatos-27b` worked fine. Now uses `ascii_downcase` on both sides. Also added `./scripts/load_bundle.sh` to the "build it first" hint shown on miss. - Stale doc claim that `make build` "uses the bundled Thanatos-27B.Q4_K_M.gguf". It doesn't — `scripts/build.sh` defaults to `${ROOT}/Qwen3.6-27B-${QUANT}.gguf` and pulls from `unsloth/Qwen3.6-27B-GGUF` if missing. README TL;DR + Quick start + Makefile comments updated. To actually load the in-repo bundle, use the new `make load-bundle`. ### Changed - **BREAKING (loaders).** Bundle re-stamped from `general.architecture: 'qwen35'` to `'qwen36'` — the architecturally-honest label. No released llama.cpp / Ollama recognizes `qwen36` yet (reconfirmed 2026-05-19 against llama.cpp 389ff61 and Ollama 0.24.0), so `ollama run hf.co/FoolDev/Thanatos-27B` and direct `llama-server -m Thanatos-27B.Q4_K_M.gguf` both fail with `unknown model architecture: 'qwen36'` until upstream adds the entry. To load *today*, rebadge to qwen35 locally with `scripts/rename_arch.py --from-arch qwen36 --to-arch qwen35` — see the README "Architecture" section for the one-liner. Tensor data stays byte-identical; only metadata flips. Reverses prior commit `f0d70ee` which had restored qwen35 for compatibility; deliberate re-flip with eyes open about the breakage. - README "Heads up" callout near the top of the model card, TL;DR, loader matrix, Quick start Ollama block, and Modelfile preamble all updated to flag the qwen36 stamp and point users at the rebadge workaround. Quick-start option B (`make build` from unsloth) called out as the loads-today path, since unsloth still ships qwen35-stamped GGUFs. ### Added - README "Vision via llama.cpp" subsection now leads with the `llama-server --mmproj` HTTP path (always built into stock llama.cpp, no extra cmake targets needed), reconfirmed working 2026-05-19 with llama.cpp 389ff61 + Vulkan on the Strix Halo reference machine (bundled Q4_K_M GGUF + `mmproj-F16.gguf` from `unsloth/Qwen3.6-27B-GGUF`). Sent a 1024-px JPEG via an OpenAI-style `image_url` data-URL content block; model produced an accurate description (Japanese alleyway with paper lanterns, bicycles, etc.) in 478 completion tokens. The visible answer arrived in `message.content`, the thinking trace in `message.reasoning_content` — the section notes both, plus the ≥500 max_tokens budget needed so the reasoning block doesn't crowd out the final answer. The existing `llama-mtmd-cli` and `llama-cpp-python` examples are still listed; `llama-mtmd-cli` now carries a note that it's a separate cmake target — a plain `cmake --build build` produces it, but a selective build (e.g. `cmake --build build --target llama-cli llama-server llama-bench`) silently skips it, which is what tripped the retest on this box (`LLAMA_BUILD_TOOLS=ON` was already set; the mtmd target just hadn't been requested). - Third measured tok/s data point on the Strix Halo reference hardware: **Q3_K_S under Vulkan → 12.31 tok/s aggregate** (6182 tokens / 501.9 s; 12.67 / 12.55 / 12.25 short/medium/long). Now apples-to-apples with the Q4_K_M Vulkan measurement (9.31 tok/s) — Q4_K_M is ~24% slower at this quant gap on this hardware. Also surfaces a backend delta: the older ROCm Q3_K_S snapshot was 10.14 tok/s, so Vulkan is ~21% faster than ROCm on the same hardware for this model. Modelfile hardware notes restructured to group by backend; README "Hardware requirements" reference paragraph updated to the Vulkan numbers with the ROCm comparison kept inline. - README "Architecture" section: a reproducible recipe for testing the `qwen36` arch path locally (`scripts/rename_arch.py` + an inline Modelfile + `ollama create` + a one-line `ollama run`). The intent is a fast way to check "is upstream support here yet?" — today it errors with `unknown model architecture: 'qwen36'`, the day it succeeds is when the bundle gets flipped. ### Fixed - README qwen36-retest recipe had two bugs that surfaced on reconfirmation against Ollama 0.24.0 on 2026-05-19: 1. `echo "FROM ./Thanatos-27B.Q4_K_M.qwen36.gguf" > /tmp/Modelfile.qwen36` never resolves: `ollama create` looks up a relative `FROM` against the Modelfile's directory (`/tmp`), not the user's CWD. The CLI fails with `400 Bad Request: invalid model name` before the loader runs, which is misleading. Recipe now uses `FROM $PWD/Thanatos-27B.Q4_K_M.qwen36.gguf`. 2. The CLI error users see today is no longer `unknown model architecture: 'qwen36'` — Ollama 0.24's scheduler wraps the loader and surfaces only `500 Internal Server Error: unable to load model: `. The arch-unknown line still appears in the server log (`journalctl --user -u ollama`). README now spells out where to look for the real cause and stamps the reconfirmation date. - Canonicalized HF repo URL case throughout docs and scripts: `FoolDev/thanatos-27b` → `FoolDev/Thanatos-27B` (the actual HF repository name). Touched README.md, examples/README.md, examples/ollama_chat.py, CITATION.cff, scripts/smoke_test.sh, scripts/check_bridge_sync.py, and the earlier CHANGELOG entry that described the rename plan as if it targeted the lowercase form. Local Ollama tag `thanatos-27b` (set in the Makefile / build.sh / smoke_test.sh / bench.sh defaults) stays lowercase — only the `FoolDev/` URL was capitalized. HF's 307 redirect resolves the lowercase form transparently, but linking to the canonical case avoids relying on that redirect and matches the HF web UI. - `examples/README.md` "Why not Ollama?" section: same `qwen35`/ `qwen35moe` status split applied earlier to the Modelfile / README / `llama_cpp_vision.py` — text via the Go engine works on 0.24+, vision via the C++ fallback path still blocked by [ollama/ollama#15898](https://github.com/ollama/ollama/issues/15898). - `examples/llama_cpp_vision.py` "Why this script exists" preamble: replaced the overbroad "Ollama 0.22's vendored llama.cpp fork is missing the qwen35/qwen35moe arch entries" claim with the same split the README and Modelfile now use — Go engine has the entries (text works on 0.24+), C++ fallback used when mmproj is attached still lacks them. Issue link unchanged (#15898 still open). Keeps the script's "use llama.cpp directly for vision" recommendation intact, just stops misleading users about which Ollama codepath is broken. - `examples/ollama_chat.py` simple-chat demo no longer silently swallows the thinking trace on Ollama 0.24. The demo parsed `...` blocks out of `message.content` — a workaround dating to Ollama 0.22 when reasoning was inlined into `content`. On the current build (especially with `OLLAMA_NEW_ENGINE=1`) Ollama returns thinking in a dedicated `message.thinking` field and keeps `content` clean, so the regex extracted nothing and the demo printed `[answer]` only. Now reads `message.thinking` first and falls back to `` extraction for older builds; docstring updated to match. ### Added - Second measured tok/s data point on the Strix Halo reference hardware (ASUS ROG Flow Z13 GZ302EA / Ryzen AI Max+ 395 + Radeon 8060S iGPU, 32 GB unified, gfx1151): **Q4_K_M, num_ctx 16384, 3-prompt mix → 9.31 tok/s aggregate** (5356 tokens / 574.9 s; 9.48 / 9.43 / 9.28 short/medium/long), measured under the systemd unit's tuned config (`OLLAMA_FLASH_ATTENTION=1`, `OLLAMA_KV_CACHE_TYPE=q8_0`, `OLLAMA_VULKAN=1`). About 8% slower than the existing Q3_K_S entry (10.14 tok/s) on the same hardware, consistent with the larger weights (17 GB vs 12 GB). Modelfile hardware notes + README "Hardware requirements" reference block updated. The earlier change-set's "verified locally … at ~10 tok/s" CHANGELOG note was an estimate; this is the real number. ### Fixed - Reverted accidental `general.architecture` rebadge of the bundled GGUF (`qwen35` → `qwen36`). Commit 2dbe526 stamped the Q4_K_M GGUF with `arch='qwen36'` (via the new `scripts/rename_arch.py`); no released ollama or upstream llama.cpp recognizes that arch string, so `ollama run hf.co/FoolDev/Thanatos-27B` failed at load with `unknown model architecture: 'qwen36'`. Reverted in f0d70ee — LFS pointer back to the original `qwen35` blob, same Q4_K_M tensor data as upstream `unsloth/Qwen3.6-27B-GGUF`. The mirroring rebadge on the sibling `FoolDev/Janus-35B` (`qwen35moe` → `qwen36moe`) was also reverted in that repo's commit ee40cb2. ### Added - `scripts/rename_arch.py` — generic GGUF arch renamer. Walks the KV section, renames `general.architecture` and every `.*` namespaced key, and rewrites tensor data byte-for-byte (no quant change). Used once for the `qwen35` → `qwen36` experiment above (since reverted); kept in `scripts/` as a reusable tool for future arch renames if needed. Picked up by `scripts/check.sh` via the existing pyflakes / py_compile passes. - `.gitignore` pattern `Thanatos-27B.*.qwen[0-9]*.gguf` to exclude local rebadge outputs from `rename_arch.py` so they don't accidentally land in LFS and ship via the HF Ollama bridge. The existing `!Thanatos-27B.*.gguf` allow-rule was permissive enough to catch the rebadged copy when adding files in bulk. ### Changed - README "Loader compatibility" table + `## Known limitations` bullet + Modelfile header comment: split the Ollama `qwen35` / `qwen35moe` status into two halves. Text inference works in current ollama (the Go engine has the arch entries — verified locally with the bundled Q4_K_M GGUF at ~10 tok/s on Strix Halo); only the C++ llama.cpp fallback — which Ollama switches to when an mmproj is attached — still lacks them, so vision (mmproj) remains broken upstream while text is fine. Earlier wording implied the entire vendored fork lacked the arch entries, which was overbroad and would mislead users into expecting text to fail too. Bumped the table's "Ollama 0.22" version reference to 0.24. Upstream tracking issue [ollama/ollama#15898](https://github.com/ollama/ollama/issues/15898) is still open. ### Renamed - **Project renamed `Janus-27B` → `Thanatos-27B`.** The 35B-A3B MoE sibling (`FoolDev/janus`) keeps the Janus name; this repo splits off as its own identity. In-repo rewrite covered: `README.md`, `Modelfile`, `params` / `system` / `template`, `Makefile`, `CITATION.cff`, `.gitignore`, `examples/**`, `scripts/**`. The bundled GGUF was renamed `Janus-27B.Q4_K_M.gguf` → `Thanatos-27B.Q4_K_M.gguf` (`git mv`, LFS pointer preserved). Default Ollama tag flipped `janus-27b` → `thanatos-27b`. System prompt identity flipped "You are Janus" → "You are Thanatos" everywhere it appears (`Modelfile`, `system`, `examples/*.py`). Sibling references back to `FoolDev/janus` and `Janus-35B-A3B` are preserved on purpose — those still point at the MoE. - **HF-side rename done:** the HF repository was renamed `FoolDev/janus-27b` → `FoolDev/Thanatos-27B` in HF Settings → "Rename or transfer this repository". One-time URL break is unavoidable: existing `ollama pull hf.co/FoolDev/janus-27b` callers now 404. (Case is canonical `Thanatos-27B`; HF's 307 redirect resolves `thanatos-27b` to it transparently, but in-repo docs use the canonical form throughout.) ### Changed - `banner.svg` wordmark `JANUS-27B` → `THANATOS-27B`. Font-size dropped 26 → 22 to keep the longer 12-char wordmark inside the same 383×77 viewBox without encroaching on the activation-grid dots in the upper right. Tokyo Night palette unchanged: `#c0caf5` mark, `#bb9af7` highlight on the suffix, gradient backdrop, animated activation grid + token-stream beam preserved. - `banner.png` regenerated from the updated `banner.svg` via `rsvg-convert` at the same 383×77 dimensions. - `dense-flow.svg` left unchanged — it has no wordmark, only the 64-layer hybrid-attention pulse visualization. ### Added - README top-row badge linking to [`buymeacoffee.com/Thanatos-27B`](https://buymeacoffee.com/Thanatos-27B). Tokyo Night yellow (`e0af68` on `1a1b26` labelColor) so it sits alongside the existing License/Base/Arch/Sibling badges without visual fight. Tip-jar only — no functional change to the model or tooling. ### Removed - `Janus-27B.Q3_K_S.gguf` no longer redistributed in this repo. Removing it leaves `Janus-27B.Q4_K_M.gguf` as the only GGUF, which flips HF's Ollama bridge default-tag pick from Q3_K_S to Q4_K_M unambiguously: `ollama run hf.co/FoolDev/janus-27b` now resolves to the 17 GB Q4_K_M blob instead of the 12 GB Q3_K_S blob. This is the resolution to the long-running "Q4 to widget" thread — rename hacks broke HF's GGUF detection (see earlier "Investigated (and reverted)" entry), and HF doesn't expose a documented default-quant metadata field, so the only reliable fix was to remove the conflicting file. Q3_K_S users build it locally with `make build QUANT=Q3_K_S`, which downloads `Qwen3.6-27B-Q3_K_S.gguf` from `unsloth/Qwen3.6-27B-GGUF` and patches the Modelfile FROM line into a temp copy. Tradeoff: 12 GB less data shipped through the repo, smaller-footprint users do an extra ~3 minute download, default Ollama tag now matches the README's recommendation. Updated Modelfile / README / examples / CITATION.cff to reflect Q4_K_M as the only bundled quant. ### Investigated (and reverted) - Tried renaming `Janus-27B.Q3_K_S.gguf` to `Janus-27B.q3_k_s.gguf` (lowercase quant suffix) hoping to flip HF's default-`:latest`-tag pick from Q3_K_S to Q4_K_M. The theory was that HF picks the alphabetically-first GGUF and lowercase 'q' (0x71) sorts after uppercase 'Q' (0x51), so Q4_K_M would become first. **Result: HF stopped recognizing the repo as GGUF entirely.** After commit 6973bea (since reverted) the HF API returned `gguf.total: null` and `gguf.totalFileSize: null`, and `ollama pull hf.co/FoolDev/janus-27b` failed with `400 Repository is not GGUF or is not compatible with llama.cpp`. HF's GGUF-detection regex evidently requires the uppercase-quant pattern, so lowercasing it broke the integration entirely — worse than the original Q3_K_S-default behavior. Reverted to `Janus-27B.Q3_K_S.gguf`. The "Pin the explicit `:Q4_K_M` tag" documentation in the README / examples / docstring stays as the practical fix; we don't have control over HF's default-pick logic. ### Added - `scripts/smoke_test.sh` opt-in tool-call round-trip (gated by `TOOLS_TEST=1`). The previous smoke test only verified the `tools` capability is *advertised* (template references `.Tools` / `.ToolCalls`); it did not verify the model actually emits a parseable tool call. A regression in the prompt scaffolding inside the `TEMPLATE` block can leave capabilities reported correctly but tool emission failing — the assistant prose-describes the call instead of emitting a `{...}` block. The new section sends a tools-array request, parses `.message.tool_calls`, and asserts the shape (`name=get_weather`, `city=Tokyo` case-insensitive). Off by default to keep `make smoke` fast (~1 sec); on for `make smoke-tools` (~10 sec). Verified end-to-end against `hf.co/FoolDev/janus-27b:Q3_K_S`. - `make smoke-tools` target wraps `TOOLS_TEST=1 ./scripts/smoke_test.sh`. ### Added - `scripts/check_bridge_sync.py` — regression guard against drift between the `Modelfile` and the root-level HF Ollama bridge files (`template` / `system` / `params`). The two configurations are consumed by different code paths — `Modelfile` by `ollama create -f Modelfile` for local builds, the three bridge files by HF's Ollama bridge when users `ollama run hf.co/...` directly (HF does not read `Modelfile`). If they drift apart, the two pull paths produce different behaviour, exactly the bug fixed in commit 33458f7. The script parses the Modelfile's `TEMPLATE` / `SYSTEM` / `PARAMETER` directives, loads the three bridge files, and fails loudly on any mismatch with a per-key diff. Wired into `scripts/check.sh` so the existing pre-commit hook runs it. Verified end-to-end by deliberately mutating `params` and confirming the check exits 1 with a clear diagnostic. ### Changed - README + `examples/README.md` + `examples/ollama_chat.py` docstring: recommend the explicit `:Q4_K_M` tag (and `:Q3_K_S` as the smaller-footprint alternative) instead of the bare `ollama run hf.co/FoolDev/janus-27b` form. HF's Ollama bridge picks the alphabetically-first GGUF as the default (`:latest`), and our filenames sort as `Janus-27B.Q3_K_S.gguf` < `Janus-27B.Q4_K_M.gguf`, so the bare form pulls the 12 GB Q3_K_S blob, not the 17 GB Q4_K_M the docs previously implied is "default". Verified empirically: `ollama pull hf.co/FoolDev/janus-27b` resolves to model SHA `4afb4abcf020` (12 GB), and the HF API exposes `gguf.totalFileSize: 12358727904` for the repo. HF's published Ollama docs say Q4_K_M *should* be the default when present, but in practice the alphabetical-first heuristic wins; until that changes, pinning the tag explicitly is the only reliable way to get the recommended quant. ### Fixed - README `## TL;DR` block previously claimed the `hf.co/...` snippet "pulls a GGUF + this repo's Modelfile, including the tool-calling TEMPLATE". HF's Ollama bridge does NOT read `Modelfile` — it ingests the root-level `template` / `system` / `params` files (per https://huggingface.co/docs/hub/en/ollama). Updated the TL;DR to state that explicitly so users skimming the top of the model card don't form the wrong mental model of which file is in play. Also fixed the `make build` comment ("downloads Q4_K_M (default)") to match the current behaviour: `make build` uses the bundled `Janus-27B.Q4_K_M.gguf` from the repo without downloading. ### Added - Root-level `template`, `system`, and `params` files for HF's Ollama bridge. The bridge generates Ollama manifests at request time from these three files (NOT from `Modelfile` — confirmed against https://huggingface.co/docs/hub/en/ollama). Without them, `ollama run hf.co/FoolDev/janus-27b` got an auto-generated manifest with the broken `{{ if .Prompt }} .Prompt }}<|im_end|>` template (Ollama's faulty Go-template conversion of the GGUF's embedded jinja), corrupted stop tokens (`".Prompt }}<|im_end|>"` bleed), and no `.Tools` / `.ToolCalls` blocks — so the published Ollama tag advertised `completion` only, rejected any request with a `tools` array, and was actually broken to load (see "Fixed" below re: the projector layer). The three files mirror the `Modelfile`'s `TEMPLATE` / `SYSTEM` / `PARAMETER` directives; both routes wire tool calling correctly. Edit them together when changing one. ### Fixed - Renamed `scripts/fetch_mmproj.sh` → `scripts/fetch_vision.sh`. HF's Ollama bridge was filename-pattern-matching `mmproj*` anywhere in the repo and shipping `scripts/fetch_mmproj.sh` (a 2028-byte bash script) as the `application/vnd.ollama.image.projector` layer. When Ollama tried to load that "projector" as a GGUF, it failed the magic-bytes check and `ollama show` / `ollama run` produced `Error: invalid file magic`. Renaming the script breaks the pattern match and the projector layer is no longer added to the manifest. Updated `Makefile` (`mmproj` target) and README references to point at the new name. ### Changed - README "Tool / function calling" section: split into explicit Ollama-path and embedded-jinja-path subsections. The two loader paths produce different on-the-wire formats and the previous text conflated them. The Ollama path (this repo's `Modelfile` TEMPLATE) prompts JSON-in-XML — the form Ollama's tool-call extractor parses into a structured `tool_calls` array. The embedded-jinja path (llama.cpp, llama-cpp-python, LM Studio) reads the Qwen 3.6 native chat template baked into the GGUF, which prompts the model to emit the verbose `` / `` form it was trained on. Both are valid; the model adapts to whichever shape its system prompt prescribes. README now shows both formats with a concrete example each so users pick the parser that matches their loader. - README "Chat template" intro: clarified that Ollama is the lone loader that needs the `Modelfile` `TEMPLATE` override; everything else gets correct plain-conversation formatting from the embedded jinja directly. Previous wording implied all loaders handled it automatically. ### Fixed - `examples/ollama_chat.py` docstring: step 2 ("Edit ../Modelfile so the FROM line points at the GGUF path") was stale — commit 3d2e907 pointed the bundled `Modelfile` at `./Janus-27B.Q4_K_M.gguf`, which the repo now ships, so the no-edit flow `ollama create janus-27b -f ../Modelfile` works out of the box. Replaced the four-step list with two paths (bundled-GGUF or pull-from-HF) covering both flows. - `examples/README.md`: header said "Three minimal entry points" but the table lists four (`ollama_chat.py`, `transformers_quickstart.py`, `llama_cpp_quickstart.py`, `llama_cpp_vision.py`). - `examples/README.md` "Or build locally with a different upstream quant" snippet was stale: it told users to `hf download` Q4_K_M (now bundled) and manually edit `../Modelfile` `FROM` (the bundled file already points at `./Janus-27B.Q4_K_M.gguf`). Replaced with the canonical `make build` flow, plus a note that `make build QUANT=...` handles a non-bundled quant by patching `FROM` in a temp Modelfile copy automatically (matches what `scripts/build.sh` actually does). - `CITATION.cff` abstract: claimed "weights are pulled from upstream … rather than redistributed" but commits b0d8482 (Q4_K_M) and 4c20ab5 (Q3_K_S) added bundled GGUFs to the repo. Updated to acknowledge the two redistributed quants while noting other quants + safetensors are still pulled on demand. ### Added - `Modelfile` hardware notes: log a measured data point for the ASUS ROG Flow Z13 GZ302EA (Ryzen AI Max+ 395 / Radeon 8060S iGPU, 32 GB unified, ROCm gfx1151) — Q3_K_S at num_ctx 16384 reads 10.14 tok/s aggregate over the bench.sh 3-prompt mix (10.37 / 10.31 / 10.11 short/medium/long, 8080 tokens / 796.5 s). Backs up the README's ~10 tok/s reference number with the exact measurement that produced it. The previous `⚠ 32 GB unified-memory laptops — borderline` row stays because it covers the Q4_K_M case. ### Fixed - `Makefile`: propagate `TAG` to `scripts/build.sh`. The build target declares `TAG ?= janus-27b` at the top of the file (and lists it in the help block) but didn't actually pass it through, so `make build TAG=janus-27b-q3 QUANT=Q3_K_S` produced `janus-27b:latest` instead of `janus-27b-q3:latest`. Combined with the matching `scripts/build.sh` change in the previous commit, the override now works end-to-end via either entry point. - `scripts/build.sh`: accept `TAG` as an env override (was hardcoded to `janus-27b`, silently ignoring callers that wanted a different Ollama tag — e.g. `TAG=janus-27b-q3 ./scripts/build.sh Q3_K_S` produced `janus-27b:latest` instead of `janus-27b-q3:latest`). Default behaviour unchanged when `TAG` is unset. Also fixes cosmetic section numbering in the comments (skipped from `# ---- 2.` to `# ---- 4.` with no section 3). - `Modelfile`: change FROM from `./Qwen3.6-27B-Q4_K_M.gguf` to `./Janus-27B.Q4_K_M.gguf`. The repo now ships GGUFs (commit b0d8482 added Q4_K_M, commit 4c20ab5 added Q3_K_S), so the no-script path `git clone && ollama create janus-27b -f Modelfile` works out-of-the-box for users with LFS smudge enabled. Comment block refreshed: removed the stale "does not redistribute weights" line (contradicted by the bundled GGUFs) and added a pointer to `make build QUANT=Q3_K_S` for the smaller quant. `scripts/build.sh` still patches FROM in a temp Modelfile copy, so existing build flows are unchanged. ### Added - `examples/ollama_chat.py`: respect `MODEL` and `HOST` env vars (matches the convention scripts/smoke_test.sh and scripts/bench.sh already use). Lets you run the demo against the HF-pulled model with `MODEL=hf.co/FoolDev/janus-27b python ollama_chat.py` instead of editing the constant. - `ollama` tag in the model-card frontmatter so HF surfaces the repo in Ollama-compatible collections / filters alongside the existing `gguf` tag. - README "Local apps" subsection enumerating how Ollama, LM Studio, Jan, llama.cpp, llama-cpp-python, Open WebUI/KoboldCpp/TGW pick this model up from the bundled GGUFs. HF's "Use this model" widget auto-shows most of these once it parses the GGUFs, but the README stands on its own and notes which apps currently honor the embedded tool-calling template (Ollama via the Modelfile TEMPLATE, llama.cpp / llama-cpp-python via the GGUF's jinja). - `Janus-27B.Q4_K_M.gguf` (~17 GB) and `Janus-27B.Q3_K_S.gguf` (~12 GB) shipped in the repo so HF's "Use this model" widget surfaces the Ollama snippet (`ollama run hf.co/FoolDev/janus-27b`) and Ollama can pull weights + Modelfile in one step. Q4_K_M is the default tag; `:Q3_K_S` is the smaller-footprint option for 16 GB GPUs / unified-memory laptops. Adds `gguf` to the model-card tags so HF lists the repo among GGUF models. Loosens `.gitignore` from `*.gguf` to allow `Janus-27B.*.gguf` while still excluding the upstream `Qwen3.6-27B-*.gguf` that `make build` fetches locally. README's "does not redistribute weights" wording reworked: the wrapper philosophy still holds for the upstream Qwen GGUFs, but we ship two tagged distributions to back the HF/Ollama UX. ### Fixed - `Modelfile`: ship a `TEMPLATE` directive mirroring Qwen 3.6 ChatML in Ollama Go-template form, so Ollama's tool-capability detector sees `.Tools` / `.ToolCalls` references. After `make build`, `ollama show janus-27b` now lists `tools` and `thinking` under Capabilities, and both `/api/chat` and `/v1/chat/completions` accept a `tools` array (previously rejected with `does not support tools`). Same template as the 35B sibling — both share the Qwen 3.6 chat format. Verified end-to-end with `examples/ollama_chat.py:tool_round_trip` (model emits a ``, helper executes the stub, model produces final answer). README "Tool / function calling" rewritten and the corresponding Known-limitations bullet removed. - `Modelfile`: added explicit `PARAMETER stop` directives for `<|im_end|>`, `<|endoftext|>`, and `<|im_start|>`. Ollama was only picking up `<|im_end|>` from the GGUF metadata, so when the model emitted `<|endoftext|>` it kept generating past it and synthesised a fake new user turn. Reproducible via `make smoke` before/after. ### Added - `scripts/bench.sh` + `make bench`: tok/s benchmark using Ollama's `eval_count` / `eval_duration` response metadata (avoids client-side stopwatch noise). Mixes short / medium / long prompts so the average doesn't lock onto a single output shape, and warms up first so the model-load cost doesn't pollute the first sample. Reference data: Ryzen AI Max+ 395 / Radeon 8060S iGPU clocks ~10 tok/s at Q3_K_S. README hardware section now points readers at `make bench` to measure their own rather than trusting the estimated table. - `scripts/smoke_test.sh`: token-leakage guard. The previous round-trip check passed on any non-empty response — including the broken EOS-bleed output that motivated the stop-token fix above. The smoke test now fails hard if `<|im_start|>`, `<|im_end|>`, or `<|endoftext|>` appear verbatim in the response. - `scripts/smoke_test.sh`: tools-capability guard. Hits `/api/show` and asserts `tools` is in the capabilities list before running the round-trip. Locks in the Modelfile `TEMPLATE` work — if a future change strips or breaks the TEMPLATE so Ollama no longer detects `.Tools` / `.ToolCalls`, `make smoke` fails before the user discovers it via a `does not support tools` 400. ### Removed - `Modelfile.z13` and the `z13` build profile. The Z13-specific tuning (Q3_K_S + 8K ctx + FA + q8_0 KV cache) wasn't meaningfully different from "use a smaller quant on a 32 GB unified-memory laptop", so the separate Modelfile and the `PROFILE` plumbing in `Makefile` / `scripts/build.sh` were removing value rather than adding it. README now points 32 GB users at `make build QUANT=Q3_K_S` directly. ### Changed - README "Why a 27B variant?": the "slower per token than 35B-A3B" claim now ships with a measured comparison — 27B Q3_K_S clocks ~10 tok/s vs ~27 tok/s for the 35B at ~Q4 on the same Ryzen AI Max+ 395 / Radeon 8060S iGPU (`make bench`). Same hardware, asymmetric quants (35B doesn't ship a Q3_K_S), but the ~2.7x ratio is what you'd expect from ~3B vs 27B active params. - README + `examples/README.md` Vision sections: tightened the Ollama-0.22 failure-mode description. Previously implied that `ollama create` itself errors with `unknown model architecture`. Empirically (against dense `qwen35` 27B + `mmproj-F16`), `ollama create` succeeds, `ollama show` even reports the `vision` capability with a CLIP projector attached, and the architecture error only fires from the runner on the first inference request — at which point it blocks text inference too. Matches the upstream issue's "blocks ALL inference" phrasing. - README: added a TL;DR section right after the intro paragraph so someone scanning the page gets a working command without scrolling past Why / What's here / Architecture. - README chat-template section: replaced the cross-reference to the 35B sibling card with inlined examples (plain conversation, `` reasoning trace, XML tool call). The README now stands on its own. - Minor: dropped the "Same as the 35B sibling:" lead-in above the system prompt block; the prompt was already inlined. ## [0.5.0] - 2026-05-02 — `e4beea4` ### Added - `examples/llama_cpp_vision.py` — image-text-to-text via `llama-cpp-python` + a separate `mmproj-F16.gguf`. Currently the only working vision path for Janus-27B (Ollama is broken; see Changed). - `scripts/fetch_mmproj.sh` — pulls `mmproj-F16.gguf` (or BF16/F32) from `unsloth/Qwen3.6-27B-GGUF`. Honors `MMPROJ_PATH` override. - `Makefile`: new `mmproj` target. ### Changed - README: replaced the misleading "Multimodal: Yes (vision)" comparison row with a per-loader breakdown. Added a dedicated **Vision** section documenting: - The Ollama 0.22 architecture gap (`unknown model architecture: 'qwen35moe'`) tracked in [ollama/ollama#15898](https://github.com/ollama/ollama/issues/15898). - A working llama.cpp / llama-cpp-python path with the `mmproj`. - Updated Known Limitations entry accordingly. - `Modelfile` and `Modelfile.z13`: header comments now state explicitly that they're text-only and link to the Vision section. - `examples/README.md`: reflects the new Vision example and explains why Ollama is not the recommended backend for it (yet). ## [0.4.0] - 2026-05-02 — `9ca8700` ### Added - `Makefile` — convenience wrapper. `make help` lists targets: `build` / `smoke` / `check` / `hooks` / `clean`. Variables `QUANT`, `PROFILE`, `TAG`, `GGUF_PATH`, `MODEL` are overridable. ### Changed - `scripts/build.sh` now honors `GGUF_PATH` so users with weights already on disk (e.g. shared model dirs, NAS mounts) don't have to download or symlink. Also defers the HF-CLI requirement until a download is actually needed. - `.gitignore` now excludes `.cache/`, `*.incomplete`, and `*.lock` so ephemeral artifacts from running `hf download` inside the repo never get committed. ## [0.3.0] - 2026-05-02 — `7766f0b` ### Added - `scripts/check.sh` — local lint runner: `bash -n`, optional `shellcheck`, `pyflakes`, `py_compile`, plus a guard regex for the dash/dot filename bug. Returns non-zero on failure. - `scripts/install-hooks.sh` — installs `check.sh` as a git pre-commit hook so footguns can't slip past again. - `CHANGELOG.md`. ## [0.2.1] - 2026-05-02 — `82677d0` ### Fixed - Wrong GGUF filename pattern in `Modelfile`, `Modelfile.z13`, `scripts/build.sh`, `examples/README.md`, and `examples/llama_cpp_quickstart.py`. Upstream `unsloth/Qwen3.6-27B-GGUF` uses dashes (`Qwen3.6-27B-Q4_K_M.gguf`), not dots — without this fix `scripts/build.sh` 404s on first run. ### Changed - README architecture section corrected. Qwen 3.6 27B is **not** pure GQA; it's a hybrid stack of `[3 × Gated DeltaNet → 1 × Gated Attention]` repeated 16 times, per the upstream Qwen 3.6 docs. Added MTP head note and YaRN ~1 M context ceiling. - README comparison table: Q4_K_M is ~17 GB (was 16); added Q3_K_S row; qualified the min-host-memory entry by quant + context size. - `scripts/build.sh` now lists known-good quant suffixes inline so typos like `Q4_KM` are easier to catch. ## [0.2.0] - 2026-05-02 — `6f2884f` ### Added - `LICENSE` (Apache-2.0) and `CITATION.cff`. - `Modelfile.z13` — Q3_K_S, 8K ctx, FA + q8_0 KV cache profile that fits in the Ryzen AI Max+ unified memory pool (~17 GB total). - `examples/`: - `ollama_chat.py` — chat, streaming, tool round-trip, OpenAI-compat - `transformers_quickstart.py` — bnb 4-bit by default, `--no-4bit` for bf16 - `llama_cpp_quickstart.py` — daemon-free GGUF inference - `examples/README.md` — picks between the three - `scripts/build.sh` — one-shot GGUF pull + `ollama create` for default and z13 profiles. - `scripts/smoke_test.sh` — server-reachable + model-present + round-trip. - `.gitignore`. ### Changed - README updated to reference the new files; replaced the "borderline" Z13 caveat with a working profile (`Modelfile.z13`). ## [0.1.0] - 2026-05-02 — `b564869` Initial public release of the Janus-27B model card. ### Added - README.md, banner.svg / banner.png (Tokyo Night themed), `Modelfile`, `.gitattributes`.