Thanatos-27B-HERETIC / CHANGELOG.md
FoolDev's picture
Claude Opus 5 (1M context)
release 0.12.17: 0.12.16's correction named one gap where there were two
35da18d
|
Raw History Blame Contribute Delete
223 kB

Changelog

All notable changes to this repository. Format loosely follows Keep a Changelog. 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=<yours> (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 <tag> 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 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 <tools> preamble is prose to every other assertion here, so the system block could teach <function_call> while the parser scanned for <tool_call> 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 <think>\n\n</think> 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 <tool_call> 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 <tool_call>, 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 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 </think> 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 <tool_call> 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 <tool_call>, the thinking tags <think> / </think>. 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: <tool_call> for 0.12.4 (2/2 first-turn calls parsed), { for 0.12.5 (0/5), <tool_call> 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 <tool_call> 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 <tool_call> 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 <think>\n…\n</think> 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 </think>, 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 <tool_response> 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 <tool_call>; it was a single newline.
  • Replayed reasoning is wrapped as <think>\n…\n</think> plus a blank line, and a turn that did no reasoning renders the empty <think>\n\n</think> 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 <think> 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 <think> 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 <function=get_weather><parameter=city>, 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 <tool_call>, <function= and <parameter=) and parses the model's reply with a parser, and a lazy grammar triggered on <tool_call>, that accept only <function=NAME><parameter=ARG> - 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 <parameter=city> 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 <think>…</think>. 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 <think>\n\n</think> 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: 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, 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, with this repo's findings posted there (comment): 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 <tool_call> 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 <nil> <nil> [city] {...}}} inside <tools>, 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", <tool_response>). 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 <think>/</think>. 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 <think> before emitting it once after </think>, 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 <think> 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 <think> 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:

    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 <think> 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 <think> 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 — 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, 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.<last>.* 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=<any> 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 <think> 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 Modelfilebench-provenance note still saidrebundle pendingafter the rebundle shipped → aligned to the README's "figures carry over — re-measure withmake bench`".
  • Ignore build.sh's *.stripped.gguf intermediates. A make build GGUF_PATH=<bundled blob> 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 GBat 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 <think> 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 <think></think> when thinking is explicitly off, an open <think> 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 <think> 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 — 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 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: <target>] 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 <tag> 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 cps the original manifest to a .heal-backup sidecar before the rewrite, runs ollama show against the rewritten manifest, and on failure mvs 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). 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 <think> 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 <tag> + 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: <blob path>. 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/<repo> 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.
  • 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 <think>...</think> 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 <think> 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 <old>.* 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 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. 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 <tool_call>{...}</tool_call> 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 <function=name> / <parameter=arg> 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 <tool_call>, 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, <think> 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.
    • 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.