Commit Graph
4354 Commits
Author SHA1 Message Date
claude-botandClaude Opus 5 73db641d47 chore(paperless-eval): re-enable the rejected candidates as probe targets
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Not for scoring. Their classification numbers were taken against a slot that
may already have been corrupted, so the only honest use for them now is finding
where each one's prompt-length boundary actually sits.

Also repairs qwen3-30b-a3b's row, which kept the old repo:QUANT shape because
it was commented out when the file moved to explicit filenames.

Probing hermes4-14b and qwen3-8b first: both were written up as capability
failures on evidence that cannot support it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 15:02:33 +02:00
claude-botandClaude Opus 5 220bf6bfc3 feat(paperless-eval): check the instrument, don't assume it
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was canceled
The tests have to get better, and this is the change that would have caught
today's whole error class on document one instead of after six candidates.

Every AI run now asks "Nenne die Hauptstadt von Frankreich" before the
documents, every ten documents, and again when it aborts. No Paris, no run.

Why it goes mid-run and not only at the start: failures are not always
clustered. Ministral-3 3B failed 29 of 50 scattered through the run, which a
start-of-run check would have passed straight through. A slot that dies at
document 12 otherwise produces 38 more "failures" that are one event.

What the harness did wrong, so it is written down rather than relearned:

  - it measured classification accuracy while a confound (slot corruption)
    dominated, and never established that it could tell the two apart
  - it counted sequential failures on a shared slot as independent evidence
  - it recorded status codes and token counts and never once looked at what the
    model actually emitted — twice the answer was one curl away
  - it treated my own bounds, --n-predict 768 and a 300 s timeout, as
    properties of the models hitting them

A run whose canary breaks is void, not a result. Failures adjacent to the break
are one fault, and probe-length.sh says where the model's boundary really is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 15:01:45 +02:00
claude-botandClaude Opus 5 85768cd7e4 feat(paperless-eval): probe the length boundary — the test that should have been first
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Everything this harness measured about failing candidates was the wrong thing.

On this Vulkan/Iris Xe stack every model has a prompt length past which
llama.cpp stops producing language and repeats a single token — " de de de",
" the the the", " phosphorus phosphorus", or whitespace. Silent: HTTP 200,
finish_reason "stop", nothing in the log. And stateful: one request past the
boundary breaks the slot for everything after it until the pod restarts.

Which means a 50-document run measures where each model's boundary sits
relative to paperless's ~5300-token request, and nothing else. "Failed 3 of 3"
is one measurement, not three — the first document took the other two with it.

Demonstrated on gemma-4-26B-A4B, which I had written up as failing on
capability:

    fresh pod, "Nenne die Hauptstadt von Frankreich"   -> "Die Hauptstadt von
                                                          Frankreich ist Paris."
    same pod after one 5000-token request              -> " phosphorus phosphorus..."
    fresh pod, 2334-token prompt, no tools at all      -> whitespace

The model is fine. Its boundary is under 2334 tokens, so it can never serve a
paperless request, and every classification number I recorded for it described
a corrupted slot.

So the size threshold, the generation theory and the capability rankings are
all withdrawn. What correlated with success was having a boundary above 5300 —
Qwen3.6 measured coherent at 13588 — not being larger or newer.

probe-length.sh answers that in minutes instead of ninety. A fresh pod per
size, because probing sizes in sequence against one server would measure the
first bad request and nothing after it.

The galling part is that this defect is already documented in my own notes from
this morning, boundary and slot-poisoning both, and I did not apply it when
reading the results.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 14:52:22 +02:00
claude-botandClaude Opus 5 5f2955cdde docs(paperless-eval): the Q8 comparison was invalid, and why
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Final numbers were 30 scored, 20 failed — and all 20 failures sat exactly on
the 768-token cap. That does not measure precision, it measures how often a
wordier model hits a bound I chose. Discarded rather than recorded.

Q8's uncapped answers run 460-520 tokens against Q4's 280-443, roughly 60%
longer for the same task, which is enough to turn one cap hit in fifty into
twenty. Re-running at --n-predict 2048 with a 600 s client bound.

Q4's baseline stays comparable despite running at 768: it hit that cap once in
fifty documents, so raising it can move at most one document. Stated rather
than assumed.

README now documents both bounds as harness properties rather than model
properties, with the rule that follows: before comparing two configurations,
check that neither is sitting on the cap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 13:56:33 +02:00
claude-botandClaude Opus 5 7136a39f3a fix(paperless-eval): the client timeout is a second bound, and it hid the first
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was canceled
Retrying gemma-4 at --n-predict 4096 did not answer the question — it moved the
wall. At 6.6 tok/s a 4096-token budget needs 620 s of generation, and the
client bound was still 300, so the run failed with TimeoutError instead of
hitting the cap. Two bounds of mine, and lifting one just exposed the other.

EVAL_TIMEOUT is now an environment variable and run-candidate.sh takes it as a
fourth argument, so a diagnostic can raise both together:

    ./run-candidate.sh gemma4-26b-a4b 3 4096 900

What the aborted attempt did show: gemma-4 reached 1634 generated tokens and
was still going when the clock ran out. A model needing 900 tokens for a valid
call would have finished long before that, so this looks like genuine
non-termination rather than a cap set too tight — but "looks like" is exactly
the reasoning that put 768 in here unexamined, so it is running properly now.

The same caveat applies to the Q8 comparison and I have not resolved it. Q8's
uncapped responses run 460-520 tokens against Q4's 280-443, roughly 60% longer,
and 20 of its requests hit 768 against Q4's one. So "Q8 performs worse" may be
"Q8 is more verbose and my cap catches it", which is a much weaker claim. That
comparison needs redoing at a raised cap before its numbers mean anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 13:53:50 +02:00
claude-botandClaude Opus 5 4ba68b91aa fix(paperless-eval): 768 is my number, not the model's — make it settable
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Fair challenge I had not put to myself: --n-predict 768 is a bound I chose this
morning because Qwen3.6 once ran to 10269 tokens and cost 900 s a document. It
is not a property of any model.

Which means every candidate recorded as failing at exactly 768 generated
tokens — Hermes-4-14B, gemma-4-26B-A4B, both Ministrals, Granite, Qwen3-8B — is
failing against MY bound, and I wrote them up as incapable. A model that needs
900 tokens to emit a valid tool call is indistinguishable, in that record, from
one that never terminates at all.

run-candidate.sh takes the cap as a third argument:

    ./run-candidate.sh gemma4-26b-a4b 5 4096

gemma-4 is re-running at 4096 against the same three documents it failed, since
it is the newest model in the field and the cleanest failure of the set. If it
produces valid calls given room, six rejections in this file are wrong and the
size threshold I committed an hour ago is built on an artefact. If it still
runs to the new cap, the cap only changed how long it took to find out.

Worth the node time either way. A working answer takes 241-291 tokens, so 768
looked generous — but "looked generous" is not evidence, and I should have
tested the bound before drawing conclusions from models hitting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 13:46:02 +02:00
claude-botandClaude Opus 5 47ed4383f1 docs(paperless-eval): the size threshold survives a deliberate attempt to kill it
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
gemma-4-26B-A4B aborted 3 of 3, all HTTP 400 rather than timeouts, and the
server log shows it ran to the --n-predict cap on every one — the same
capability failure as the small dense models, not a template mismatch.

It was chosen precisely because it could have refuted the threshold: newest
generation in the whole field, 26B total. It did not.

  Qwen3.6-35B-A3B   35B   newest    works
  Qwen3.5-35B-A3B   35B   older     works
  Qwen3-30B-A3B     30B   oldest    fails
  gemma-4-26B-A4B   26B   NEWEST    fails
  Hermes-4-14B      14B   —         fails
  dense 3-8B        —     —         fail

Two generations work at 35B total; everything at 30B and below fails. After
three hypotheses that died on contact, this is the first one that predicted a
result in advance and was right, so it becomes the operating rule: do not spend
a screening slot on anything under ~35B total.

Deliberately not pinning it further. Whether the line is at 31B or 34B is
interesting and changes no decision — there are two working candidates and the
useful next number is the second one measured over all 50 documents, which is
now running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 13:36:43 +02:00
claude-botandClaude Opus 5 ec849f2e29 docs(paperless-eval): glm47-flash is out on speed, and settles nothing
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Aborted 3 of 3 — but every failure was a TimeoutError rather than an invalid
answer, so the experiment does not answer the question it was started for.
Document 1 did reach the --n-predict cap without producing a tool call; 2 and 3
were cut off by the 300 s client bound before anything could be concluded.

Disqualified on speed regardless, and that part is unambiguous: 31 tok/s
prefill against Qwen3.6's 100 and a dense 8B's 240, plus 4.9 tok/s decode. That
is over seven minutes per document, about six hours for a full run.

Prefill is now recorded in candidates.tsv as a selection criterion. It varies
more than sixfold across architectures and is roughly half of a run's wall
clock, and I had not been tracking it at all.

gemma4-26b-a4b replaces it as the size-versus-generation discriminator: 26B
total, well under 35, newest generation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 12:50:58 +02:00
claude-botandClaude Opus 5 43634e46c9 fix(paperless-eval): the startup budget was sized for the wrong thing
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
GLM-4.7-Flash was killed mid-download at 17.8 GB and restarted from the
beginning. The startupProbe allowed 60 * 20s = 20 minutes, and the comment
explaining that number gives the reason it was wrong: "~20 GB of weights off
CephFS". It was sized for reading a warm PVC, not for a first download from
HuggingFace, which is what actually happens the first time a candidate runs.

Too small a budget here is not a slow start — the container is killed, restarts,
and downloads again, so it never finishes. Raised to 180 * 20s = 60 minutes.

Worth writing down because the signature is misleading: a probe kill shows as
exitCode 137 with reason "Error", not OOMKilled, and 137 reads like a memory
limit. The pod meanwhile sits in Init, which by now has hidden three separate
faults in this harness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 12:31:03 +02:00
claude-botandClaude Opus 5 7658cf8bb6 docs(paperless-eval): precision is nearly free on this MoE, and my formula was wrong
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Measured, same model, same documents, only the quantisation differs:

    Qwen3.6-35B-A3B Q4_K_M   ~21 GB   11.0 tok/s
    Qwen3.6-35B-A3B Q8_0     ~34 GB   10.3 tok/s

I predicted ~6 tok/s for Q8 — a halving, on the reasoning that doubling bytes
per active parameter doubles what crosses the memory bus. It cost ten percent.

So decode is not bandwidth-bound for this MoE at all. It is compute-bound on
many small per-expert matmuls, which retroactively explains a measurement I
recorded but never followed through: the MoE reached 21.4 GB/s against a
53.9 GB/s ceiling. It was already leaving 60% of the bus idle, so more bytes
cost almost nothing.

That formula has been steering decisions in this file — it is what ranked
candidates by size and what I used to predict runtimes. It predicts dense
models and not this one.

Practical consequence: higher precision is nearly free on an MoE here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 12:19:58 +02:00
claude-botandClaude Opus 5 1ae0e59ea6 docs(paperless-eval): qwen3.5-35b-a3b passes, killing the generation theory
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
5 of 5, no failures, 584 s. Qwen3.5 is a generation older than the working
Qwen3.6 and handles the 14-field schema cleanly, so "only the newest models can
do this" does not survive either. That is the third hypothesis to die here.

What the two working models share is 35B total parameters against the failing
MoE's 30B:

  Qwen3.6-35B-A3B   35B total   newest   works
  Qwen3.5-35B-A3B   35B total   older    works
  Qwen3-30B-A3B     30B total   oldest   4 of 5 failed

Confounded, and I am not going to call it. Qwen3-30B-A3B is both smaller and
older; three points cannot separate size from generation.

glm47-flash is the experiment that can: 30B total, newest generation. Works
means generation matters and 35B is not a threshold. Fails means total size is,
and there is no point screening anything below ~35B.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 12:04:33 +02:00
claude-botandClaude Opus 5 faebeb1594 fix(paperless-eval): the crashloop was a full disk, not an ambiguous reference
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Correcting the previous commit, which named the wrong cause with some
confidence. The llama-models volume was 100% full — 120 GiB, nothing free.
llama-server could not write its metadata cache, so it could not resolve the
repository listing, and reported

    no GGUF files found in repository <repo>
    Available GGUF files:
      - <whatever it had cached before the disk filled>

That reads as a bad model reference, and I read it that way. It is a full disk.

Nine screened candidates filled the volume: 2.5 GB for a small dense model,
17-25 GB for an MoE, 34 GB for a Q8. Freed 56.5 GB by removing the weights of
candidates already rejected — they are re-downloadable, so that costs only
bandwidth.

--hf-file stays. Naming the exact file removes a genuine ambiguity (ggml-org
ships mmproj-, mtp- and dflash- variants at Q8_0 beside the model) and costs
nothing. It just was not the fault here, and the comment claiming it was is
rewritten.

README now says to check `df -h /models` before believing anything llama-server
says about a repository.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 11:48:14 +02:00
claude-botandClaude Opus 5 baf3b3e1b7 fix(paperless-eval): name the exact .gguf, the :QUANT shorthand is ambiguous
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was canceled
Two jobs crashlooped for three hours without producing a single document, and
I did not notice because the pods sat in Init the whole time — which looks
exactly like a slow model download.

    llama_model_load_from_file_impl: exactly one out metadata, path_model,
    and file must be defined

That reads as a loading error. It is an ambiguous reference. `--hf-repo
repo:QUANT` matches by substring across the entire repository, and repos
routinely carry several files bearing one tag: ggml-org ships mmproj-, mtp- and
dflash- variants at Q8_0 beside the model, bartowski ships
kld_results/kld_Q4_K_M.txt beside the weights. Q4_K_M happened to be
unambiguous for the baseline, which is why this never surfaced before.

candidates.tsv now carries the exact filename as its own column, resolved from
the HF API for all fifteen entries, and run-candidate.sh refuses to start a
candidate that has none rather than guessing. --hf-file overrides the quant.

Also fixing the monitoring gap that hid it. The watch treated PodInitializing
as "keep waiting" indefinitely, so a container failing to start was
indistinguishable from one still pulling 34 GB. It now checks init container
states for CrashLoopBackOff/Error/ImagePullBackOff and reports with the last
lines of the failing container's log.

Silence was not success, and the tooling should have said so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 11:45:00 +02:00
security-bot a358e8cfd5 chore(image-automation): bump ghcr.io/music-assistant/server:2.10.2
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-09-04 09:22:42 +00:00
claude-botandClaude Opus 5 319968c0f1 docs(paperless-eval): the MoE hypothesis is dead too
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
qwen3-30b-a3b — MoE, 30B total, 3B active, the shape that was supposed to
explain the whole pattern — scored 1 of 5 and failed 4. Indistinguishable from
the dense 8B it was meant to outclass.

That is the second architectural theory to die here. First "a smaller dense
model gains at both ends", which the timings contradicted. Now "capability
comes from total parameters, so MoE gets it cheaply".

  Qwen3.6-35B-A3B   MoE 35B/3B   Apr 2026   2 of 50 failed
  Qwen3-30B-A3B     MoE 30B/3B   older      4 of 5 failed
  Qwen3-8B          dense 8B     older      4 of 5 failed
  Hermes-4-14B      dense 14B    —          3 of 3 failed

Neither architecture nor parameter count predicts success. The only thing that
correlates so far is how recent and capable the model is — which is an
unsatisfying hypothesis, because unlike the other two it predicts nothing
cheaply and cannot be used to narrow a search.

qwen35-35b-a3b is the experiment that isolates it: identical architecture,
total and active parameters to the working baseline, one generation older. If
it fails, generation is the variable and every shape argument in this file was
noise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 08:41:16 +02:00
claude-botandClaude Opus 5 043d1af4d4 feat(paperless-eval): four more MoE candidates, and Gemma was wrongly excluded
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Five dense models have failed and the one MoE works, so the list was searched
again for that shape specifically — 20-60B total, under ~6B active:

  glm47-flash          GLM-4.7-Flash, 30B/3B, 17.8 GB
  gemma4-26b-a4b       gemma-4-26B-A4B-it, 26B/4B, 16.9 GB
  qwen35-35b-a3b       Qwen3.5-35B-A3B, 35B/3B, 21.5 GB
  nemotron35-30b-a3b   Nemotron-3.5-Lightning-30B-A3B, 30B/3B, 25.5 GB

Correcting an exclusion of mine: Gemma is not out. Gemma 3 has no tool support
at any size — verified, "tool" appears zero times in its template — but Gemma 4
26B-A4B does, and claims 140+ languages pretrained. Writing off a family by its
previous generation was sloppy.

Two carry a Vulkan risk that has to be checked in the server log rather than
assumed: Nemotron-3.5 interleaves real Mamba2 layers and Qwen3.5 uses DeltaNet
linear attention, and llama.cpp's Vulkan SSM shaders are less exercised on Iris
Xe than on Arc. Issue #19957 reported exactly this for Qwen3.5-35B-A3B, and it
turned out to be a stale build fixed well before b10731 — but a silent CPU
fallback would read as a slow model, not as an error, so confirm the layers are
offloaded.

Rejections with reasons are in the file: ERNIE-4.5-21B-A3B has tool calling
only in its Thinking variant, Ling-3.0-flash is 124B total, Granite-4.0-H-Small
is 9B active, gpt-oss-20b is a reasoning-effort model in Harmony format.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 08:30:45 +02:00
claude-botandClaude Opus 5 11b0def055 docs(paperless-eval): five dense models fail, the one MoE works
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Screening results so far, and they point somewhere I did not expect:

  qwen35        MoE 35B total / 3B active   works, 2 of 50 failed
  qwen3-8b      dense 8B                    4 of 5 failed
  hermes4-14b   dense 14B                   3 of 3 failed, aborted
  ministral8b   dense 8B                    3 of 3 failed, aborted
  ministral3b   dense 3B                    29 of 50 failed
  granite8b     dense 8B                    every document failed

Hermes is the cleanest failure of the set and the reason this is worth writing
down. Its <tool_call> JSON is an explicitly tested fixture in llama.cpp's own
test suite, so the parser demonstrably handles the format — the failure has to
be the model, not the plumbing. That also retroactively weakens the Granite
verdict, which I attributed to llama.cpp alone.

Working hypothesis: holding a long grammar-constrained structure needs model
capacity, and what supplies it is total parameters rather than active ones.
qwen3-30b-a3b tests it directly at 30B total / 3B active.

If it holds, the search for a smaller faster model is over before it started —
and the consolation is that the fastest shape on this hardware is the only one
that works, since decode cost scales with active parameters while capability
appears to scale with total.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 08:17:14 +02:00
claude-botandClaude Opus 5 71a4c1b3a7 feat(paperless-eval): measure what Q4 costs, instead of assuming it costs nothing
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Everything in this comparison is Q4_K_M, including the baseline, and the
quantisation damage has never been measured. That is an assumption sitting
underneath every number in the README.

ggml-org ships Q4_K_M, Q8_0 and BF16 of Qwen3.6-35B-A3B in one repo, so the
control is exact: same model, same 50 documents, same flags, precision the only
variable.

There is a specific suspicion behind this, not just diligence. Ministral-3
leaking [THINK] and [TOOL_CALLS] into a tool call as literal text is the kind
of fault 4-bit quantisation can produce, and Q8 would separate "the model
cannot do this" from "Q4 broke it". Three candidates have been rejected on
behaviour that no one has ruled out as a quantisation artefact.

Q8 before BF16 deliberately: ~34 GB and ~6 tok/s against Q4's ~21 GB and
11 tok/s, so roughly double the wall clock, where BF16 would be ~65 GB at
~3.5 tok/s and near five hours. If Q8 matches Q4 within the noise floor, BF16
almost certainly does too and the expensive run is unnecessary. If it does not,
BF16 becomes worth its cost.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 07:57:42 +02:00
claude-botandClaude Opus 5 0c46a219e3 feat(paperless-eval): eleven verified candidates, and what the search ruled out
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Delegated research across nine model families, every repo:quant checked against
the HF API by filename and byte size, and every chat template inspected for
tool support rather than assumed.

Ordered by the corrected size rule — MoE with few active parameters first,
then dense 3-9B with German named in the model's own card, then the rest:

  qwen3-30b-a3b       Qwen3-30B-A3B-Instruct-2507, 18.6 GB, ~3B active
  nemotron3-30b-a3b   Nemotron-3-Nano-30B-A3B, 24.7 GB, ~3B active
  glm4-9b             GLM-4-9B-Chat, 5.8 GB, BFCL 81.00, German explicit
  commandr7b          Command-R7B, 4.7 GB, native tool grammar, German explicit
  qwen25-7b           Qwen2.5-7B, 4.7 GB, German named, no thinking mode at all
  llama31-8b          Llama-3.1-8B, 4.9 GB, German in Meta's official set
  nemotron-nano-12b   Nemotron-Nano-12B-v2, 7.0 GB, German verified
  phi4-mini           Phi-4-mini, 2.5 GB, German documented
  qwen3-4b-2507       Qwen3-4B-Instruct-2507, 2.5 GB
  qwen35-9b           Qwen3.5-9B, 6.2 GB

The exclusions are in the file too, because they cost real search time and
should not be re-derived. Gemma 3 at every size, phi-4 14B, Aya and InternLM
have no tool support in their templates at all — inspected, not inferred.
Falcon 3 states EN/FR/ES/PT and German is simply not on it. Every German
fine-tune checked (SauerkrautLM, DiscoLM, LeoLM, Occiglot, Teuken) overwrites
its base template with a minimal ChatML loop that drops tool calling, even
where the base had it.

The most useful thing found explains the Granite failure. Current llama.cpp has
no per-vendor tool-call parser table; common/chat.cpp derives the grammar from
the model's own template via a PEG auto-parser. Hermes-style <tool_call> JSON
is an explicit tested fixture; Granite's shape is not. So the earlier note
saying llama.cpp "cannot parse Granite" is better read as its template being
outside what the auto-parser was validated against.

Two candidates dropped after verification: Qwen3.8-27B is dense 27.8B at
19 GB, on the wrong side of the size rule, and carries the only negative
German signal found anywhere — users reporting fabricated German words on its
own HF discussion page. aya-expanse-8b has no tool support.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 07:51:35 +02:00
claude-botandClaude Opus 5 17e23202d1 feat(paperless-eval): second wave of candidates, and the size rule was backwards
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was canceled
Eight candidates now, every repo:quant verified present via the HF API and
every one a ggml-org / bartowski / lmstudio-community / unsloth conversion
rather than the vendor's own.

The selection rule in this file was wrong and is corrected. It said a smaller
dense model gains at both ends, and the Ministral pair was chosen on that
basis. Measured:

    Ministral-3 8B dense, ~4.7 GB weights   7.1-8.2 tok/s
    Qwen3.6 35B-A3B MoE,  ~20 GB weights    11 tok/s

The largest model is the fastest one here. Decode is bandwidth-bound on the
ACTIVE parameter set, which for an A3B mixture is ~3B however much is resident;
total size only has to fit, and 96 GiB per node means it always does. So the
order is MoE with few active parameters, then dense 3-9B, then dense 10-14B
only for an exceptional model — that is ~4 tok/s, near three hours for 50
documents — and nothing dense above 14B.

hermes4-14b is in the list despite being on the wrong side of that line: five
screening documents are cheap, and it answers whether llama.cpp's named
Hermes tool-call parser handles what Granite's format could not. That answer
transfers to smaller Hermes sizes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 07:50:05 +02:00
k.renner b8116a1cf0 Merge pull request 'fix: [alert] KubeDeploymentRolloutStuck (ai) (#750)' (#753) from alert-fix/issue-750 into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#753
2026-09-04 05:44:34 +00:00
k.renner 3563a9e67e Merge pull request 'fix: [alert] KubeHpaMaxedOut (authentik) (#761)' (#763) from alert-fix/issue-761 into master
Reviewed-on: k.renner/homelab#763
2026-09-04 05:43:40 +00:00
k.renner 088fce90a4 Merge pull request 'fix: [alert] VeleroBackupStale (velero) (#780)' (#781) from alert-fix/issue-780 into master
Reviewed-on: k.renner/homelab#781
2026-09-04 05:43:05 +00:00
claude-botandClaude Opus 5 851e1b9ef6 feat(paperless-eval): screen candidates at five documents before measuring
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The setup is now good enough to test everything with a chance, so the gate
matters more than any single run. run-candidate.sh takes a sample size:

    ./run-candidate.sh <label> 5     screen
    ./run-candidate.sh <label>       measure

With ABORT_AFTER a hopeless candidate is rejected in about five minutes. Three
of the first four were rejections, and each was left running for an hour or
more first.

Screening deliberately uses genuine paperless requests rather than
probe-toolcall.sh. The standalone probe is cheaper and still worth a first
pass, but it passed two candidates that failed on document one — a synthesised
request cannot reproduce the RAG neighbours and the taxonomy candidate lists
that make a real prompt what it is. Document 1970 is 1976 characters and still
produces a 5333-token request.

A screen is a gate, not a measurement. Numbers are never compared across
sample sizes; the comparison happens at 50.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 07:24:24 +02:00
claude-botandClaude Opus 5 f6b0789c01 feat(paperless-eval): both remaining candidates measured, and a noise floor
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Qwen3.6-35B-A3B re-measured with --reasoning off:
72.9 / 77.1 / 85.4 / 94.9 / 44.4, two failures, 5494 s. Complement 30 both
right, 5 rescued, 4 broken, 9 both wrong.

Ministral-3 3B: 21 scored, 29 failed. Its correspondent numbers over those 21
sit within a document of Qwen3.6's, which is not a near-tie — the 21 are the
documents it could answer at all, a set selected for being easy, and the
classifier does better on them too (11 of 21 both right against 30 of 48). A
model that declines three documents in five has no usable accuracy number.
Commented out.

The re-run was worth more than the row it produced. Both Qwen3.6 runs used the
same 50 documents and differ by exactly one document on every metric — one of
nine on the catchall. That is the noise floor, and it is now written down: a
difference between two candidates smaller than about two documents means
nothing. Decoding is sampled; the same model on the same input does not answer
the same way twice.

I had called the re-run "better" at the 10-document mark on the strength of a
0-vs-1 failure count. It finished 2-vs-2. That was the same mistake as reading
a probe pass as a verdict, and it is the reason the noise floor is in the
README rather than in my head.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 06:29:50 +02:00
security-bot 9ee8cfb4bc chore(image-automation): bump ghcr.io/paperless-ngx/paperless-ngx:3.1.3
ci/woodpecker/cron/alert-monitor Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
Updates managed by image-automation-controller.
2026-09-04 03:21:52 +00:00
issue-fix-bot 325baf7047 fix: [alert] VeleroBackupStale (velero) (#780)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
2026-09-03 20:47:37 +00:00
claude-botandClaude Opus 5 44636a8268 docs(paperless-eval): ministral8b fails, cleanly this time
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The earlier attempt ended on three failures of which one was my own token
collision, so the verdict was not defensible. Re-run alone, with bartowski's
conversion, --reasoning off and its own API user: three documents, three
HTTP 400s, aborted after 531 s. Generation runs to the --n-predict cap and no
tool call ever comes back. Commented out.

The 3B of the same family gets through documents the 8B cannot, which is not
what I expected from a size ablation and is left enabled because it produces
real numbers with failures mixed in.

qwen35 is re-running on the freed node with --reasoning off. Its measured
result used the deprecated --chat-template-kwargs, and while that flag does
what it says on Qwen's template, a comparison table whose only filled row was
produced under different flags than everything after it is not one I would
trust.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 22:04:11 +02:00
claude-botandClaude Opus 5 440cbaec90 docs(paperless-eval): the conversion was not the whole story
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
I claimed bartowski's GGUF was "the whole difference between a working
candidate and a failing one" on the strength of a probe pass. The real run
disproved it: both conversions still run to the --n-predict cap against a
~5300-token paperless request.

What bartowski genuinely fixes is narrower — at probe scale mistralai's file
leaks [THINK] and [TOOL_CALLS] as text where bartowski's does not, and
llama-server defaults to -sp off, so those tokens are simply not marked special
in mistralai's conversion. That is worth knowing and is not sufficient.

Also correcting a verdict: Ministral-3 3B does not fail everything. Its run
never tripped ABORT_AFTER, which only fires when the first three documents all
fail — so at least one succeeded, and the absence of the abort was itself the
evidence. I read three error lines and called the model broken without noticing
what sat next to them.

probe-toolcall.sh now states plainly that a FAIL is conclusive and a PASS is
not, and why: it cannot reproduce the RAG neighbours or the taxonomy candidate
lists that inflate a real prompt. Document 1970 is 1976 characters and still
produces a 5333-token request. The real gate is ABORT_AFTER against genuine
requests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 21:54:06 +02:00
claude-botandClaude Opus 5 b64f971373 fix(paperless-eval): two concurrent runs were revoking each other's token
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Every parallel run so far was quietly broken, and it took a 401 sitting in the
middle of three genuine model failures to notice.

api_token() took the first superuser and deleted its existing token before
inserting a new one, because Django's authtoken holds one token per user. Both
candidates share this database, so the second job to start silently revoked the
first one's credentials. In the log it appears as

    50173: HTTP 401: b'{"detail":"Invalid token."}'

among HTTP 400s from the model, where it reads like one more broken document.
It was counted toward ABORT_AFTER, so a harness fault helped end a run and
would have been recorded as a model verdict.

Two changes. Each run now creates its own eval-<label> superuser, so the token
belongs to the run and concurrency is safe — only the user can differ, the
database cannot. And a 401 is now fatal with an explicit message rather than
another row in the error column: it is never a fact about the model, and every
number after it is worthless.

The qwen35 result predates this and ran alone, so it is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 21:42:20 +02:00
claude-botandClaude Opus 5 9ff85f7d1f fix(paperless-eval): probe at production prompt length too
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was canceled
The probe used the real 14-field schema but a 1191-token prompt, while a real
paperless request carries ~5300 tokens once the document and its RAG
neighbours are in. Ministral-3 8B passed the probe and then ran to the
--n-predict cap on the first document of the real run.

Whether that document is pathological or the probe is systematically too easy
is not yet settled — it has failed under every engine tried so far, including
the one that works. But a probe is only worth having if it fails where
production fails, and it costs nothing to make it realistic in both dimensions
rather than one. ~5000 tokens of German filler now precedes the document text.

This is the same mistake twice: the first version was too easy on schema
width, this one was too easy on prompt length, and both times a pass sent a
ninety-minute run after a model that could not do the job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 21:39:24 +02:00
claude-botandClaude Opus 5 22d92d158d feat(paperless-eval): probe tool calling in two minutes, not ninety
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was canceled
"These are all invocation errors" — correct, and I had written two of them off
as properties of the models.

probe-toolcall.sh starts a candidate, sends paperless's real 14-field
DocumentClassifierSchema, and passes only on finish_reason "tool_calls". The
payload is the actual schema rather than a stand-in, which is the entire point:
a three-field probe got a clean call out of Ministral-3 while the real schema
made it degenerate, and that false pass cost an hour of chasing prompt length.

What it turned up immediately:

  Ministral-3 8B and 3B pass with bartowski's conversion and fail with
  mistralai's own, at identical quant and flags. llama-server defaults to
  -sp off, so [THINK] and [TOOL_CALLS] should never have appeared as text at
  all — they are not marked special in mistralai's file. A conversion defect,
  not the model. Both candidates now point at bartowski and are running.

  Granite fails with bartowski too, finish_reason "stop" and no tool_calls, so
  its cause is different and unchanged: llama.cpp b10731 cannot read Granite's
  tool-call format back whoever converted it. The remaining lever there is
  --chat-template-file with a template the server can parse, or a newer build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 21:36:59 +02:00
security-bot a667920bb0 chore(image-automation): bump ghcr.io/cloudnative-pg/plugin-barman-cloud:v0.15.0
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-09-03 19:22:34 +00:00
security-bot 53fe8efa02 chore(image-automation): bump ghcr.io/cloudnative-pg/plugin-barman-cloud-sidecar:v0.15.0
Updates managed by image-automation-controller.
2026-09-03 19:21:48 +00:00
claude-botandClaude Opus 5 b00ca34783 fix(paperless-eval): abort a run once it is clear nothing works
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Three candidates in a row failed every document, and each time I let it run.
The harness now stops after ABORT_AFTER = 3 consecutive failures from the
first document, because document one already said everything the remaining
forty-nine would.

Not a ratio — a model failing a third of the time is a result worth measuring
in full. This only catches "nothing has ever worked".

What the three were, since none of them is a verdict on the model:

  granite8b     classifies correctly, but llama.cpp b10731 hands its tool call
                back as content with finish_reason "stop" and no tool_calls
                array, so llama_index raises "Expected at least one tool call,
                but got 0".

  ministral8b   given paperless's real 14-field DocumentClassifierSchema, opens
  ministral3b   a valid tool call and then fills its first string with its own
                control tokens until the cap:
                    {"title": "[TOOL_CALLS][ARGS][THINK][/THINK][THINK]...
                The grammar allows any character inside a JSON string, so those
                are "valid" and it never closes.

I chased the Ministral failure as a prompt-length problem for an hour on the
strength of a three-field probe that worked. It reproduces at a 1191-token
prompt with the real schema, and a 5799-token prompt with the simple one gets a
clean tool call — the schema is the variable, not the length.

So the first thing to establish about a candidate is not German, or speed, or
classification quality. It is whether it can emit one valid call against a
14-field schema, and that is a two-minute probe rather than a ninety-minute
run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 21:15:05 +02:00
claude-botandClaude Opus 5 c2f47e58bc fix(paperless-eval): --reasoning off, not --chat-template-kwargs
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
I wrote in the last commit that passing '{"enable_thinking": false}' to every
candidate was a no-op on templates that never read the variable. That is
wrong, and it cost a run.

Ministral-3-8B produced token salad on a 681-token prompt — far below the ~14k
corruption boundary, so not that defect:

    1UnComplete1Self1UnCompleteSDirect11S1S111Document1CompleteFromS11Un...

Every document failed, and each one generated to the --n-predict cap. The
identical request against a server started without the flag returned a clean
tool call and the correct answer. Plain German was fine throughout — "Die
Hauptstadt von Frankreich ist Paris" — so it only ever broke with tools, which
is what made it look like a model that cannot do function calling.

llama-server prints "Setting 'enable_thinking' via --chat-template-kwargs is
deprecated. Use --reasoning on / --reasoning off instead." at startup. It means
it. --reasoning off is verified to give Ministral a proper tool_calls response
before this went near a real run.

Qwen3.6's template does consume enable_thinking, so its measured result stands
— but its run used the old flag, and the configuration should be uniform
across candidates before the numbers are compared. Worth a re-run.

granite8b is commented out of candidates.tsv, and this one is not my flag: it
classifies correctly and llama.cpp b10731 simply cannot read its tool calls
back, returning them as content with finish_reason "stop". Reproduced on an
idle server both with and without the flag. Details are in the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 20:58:30 +02:00
claude-botandClaude Opus 5 a7bba7acbb feat(paperless-eval): one manifest, a candidate list, and the first result
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The point of this harness is comparing chat models, and it was shaped for
comparing engines. Four candidates meant four 300-line copies of the same Job,
which is exactly how --chat-template-kwargs ended up on the isolation pod but
not on the first eval job — two hours of empty completions before anyone
looked.

So: job-template.yaml holds the sidecar configuration once, candidates.tsv
holds the five values that differ, and run-candidate.sh substitutes them.
Adding a model is a line, not a file.

Candidates, all first-party GGUFs verified to exist with a Q4_K_M:

  ministral8b   mistralai/Ministral-3-8B-Instruct-2512
  ministral3b   mistralai/Ministral-3-3B-Instruct-2512
  granite8b     ibm-granite/granite-3.3-8b-instruct

Instruct, never Reasoning — a model tuned to think first does by design what
cost Qwen3.6 two of fifty documents. Sizes are chosen because decode is
bandwidth-bound at ~11 tok/s and prefill was 51% of the first run's wall clock,
so a smaller dense model gains at both ends; 3B against 8B is the size
ablation, granite against ministral the family one.

The classifier stays in every run as a fixed reference line, not as a quality
bar: train() fits on every non-inbox document with no holdout, so the
evaluation documents are in its training set and its 100% on document type is
memorisation. Irrelevant to ranking models — same constant under all of them —
and the README now says so rather than inviting the comparison.

First result, Qwen3.6-35B-A3B Q4_K_M over 50 documents: correspondent 70.8
offered / 75.0 recognised against the classifier's 72.0, document type 83.3
against 100, and 33.3 against 100 on the "Sonstige" catch-all. 5 rescued
against 5 broken — but two of those five are difflib rejecting a correct
answer, "GOTOOLS GmbH" against the stored "GOTOOLS", an exact prefix scoring
~0.74 under MATCH_THRESHOLD = 0.8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 20:14:15 +02:00
claude-botandClaude Opus 5 cb751cbedc fix(paperless-eval): bound generation server-side; the model does not always stop
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Three of the first eleven documents in a run never terminated. One reached
10269 tokens on a classification that needs ~300, at a perfectly healthy
11 tok/s — so it read as slowness and was neither slow nor a context problem.

llama-server's --n-predict defaults to -1, infinity, and paperless sets no
token budget of its own and waits. Nothing anywhere bounded the request. Four
documents cost most of an hour.

--n-predict 768 on the sidecar caps it, and a truncated response is better
evidence than a timeout: it shows what the model was doing instead of
stopping. The client timeout drops to 300 s and becomes a backstop.

Also:

- the degeneracy check fired on 'Rechnung 2025-5425541' — 8 letters in 21
  characters, below the 40% Latin threshold. Digits and punctuation are normal
  in a title; look for characters from other scripts directly instead of
  inferring them from a shortage of Latin ones.

- the ctx-size comment still carried the claim bisection disproved in #777,
  that 32768 corrupts inference. The boundary is prompt length.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 14:51:56 +02:00
claude-botandClaude Opus 5 34c250ad2a fix(paperless-eval): two more, found by actually running it
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Ran the harness against the eval instance instead of waiting for review,
which is the point of it not being Flux-managed. Two faults surfaced in
the first two attempts, and the third run scored 10/10 cleanly.

seed-data used `cp -a`. Preserving ownership needs CHOWN, which the
container does not have, so cp copied everything and then exited
non-zero, and `set -e` killed the pod before any sidecar started. It
copies with `-r` now and runs as uid 1000, which also gets the ownership
right — paperless writes to the LLM index while serving, and a copy made
by root would have been read-only to it.

The degeneracy check fired on every classic row. The classifier answers
with resolved ids and no `title` at all, so "empty title" is its normal
shape rather than broken inference; the check now runs for the AI engine
only. Left as it was, every classic run would have reported zero scored
documents and a 100% degenerate rate.

Validated: 10 documents, 0 failed, 0 degenerate, correspondent 80.0%,
document type 100.0% — matching what the bash version measured against
production, so the port to Python changed nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 11:08:46 +02:00
claude-botandClaude Opus 5 e261089666 fix(paperless-eval): three faults the first job run exposed
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The Job from #776 ran and failed, which was more useful than it sounds.

It died before scoring anything: psycopg returns Postgres numerics as
Decimal, and `100.0 * top / total` raises TypeError rather than coercing.
Both operands are cast now.

It also seeded nothing. The golden volume was empty, `cp -a` copied
nothing, and `du ... || true` swallowed it — so the run would have
measured the no-RAG path and reported it as a RAG result, which is
precisely the failure the initContainer was written to prevent. It now
refuses to start when llm_index is under 1 MiB, and says how to build it.

And the script had no way to tell broken inference from a wrong guess.
Past roughly 14k prompt tokens this stack returns empty completions and
then runs of digits and CJK characters, with status 200 and
finish_reason "stop", and one such request poisons the llama.cpp slot for
everything after it. Scored as wrong answers those would quietly depress
whichever candidate happened to trip it, and take the rest of that run
down with it. They are flagged, excluded from the accuracy figures, and
counted separately — a non-zero `degenerate` means the run is suspect
from that point on rather than that the model is bad.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 11:01:19 +02:00
k.renner c3a5662f72 Merge pull request 'fix(llama-server): the limit is prompt length; take the eval harness out of GitOps' (#777) from eval-out-of-iac into master
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#777
2026-09-03 08:57:34 +00:00
claude-botandClaude Opus 5 c47c2e6bc1 refactor(paperless-eval): take the measurement harness out of GitOps
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
A Job is immutable and a measurement is finished when it finishes —
neither is something Flux should converge on. Re-running a configuration
means deleting and recreating the Job, and a managed one comes straight
back. Worse, eval.py was a kustomize-generated ConfigMap, so editing the
script between runs and applying it by hand would have been reverted on
the next reconcile: the iteration loop the harness exists for was closed
off by where it was put.

The namespace stays managed. It holds a database clone, PVCs and a
network policy — state that should converge and should vanish when the
Kustomization is removed. Only the harness moves to scripts/, alongside
the other tools nobody deploys, with a README covering how to run one,
how to add a candidate, and how to read the output.

Two failure modes are written down there rather than left to be
rediscovered: prompts past ~14k tokens make this stack emit garbage
silently and poison the slot until restart, and ground truth is a filing
convention that has drifted over ten years, so the comparison between
engines is the durable result rather than the absolute numbers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 10:52:58 +02:00
claude-botandClaude Opus 5 d4e1f49d78 docs(llama-server): the limit is prompt length, not --ctx-size
The comment blamed --ctx-size 32768 for corrupting inference. Bisection
on a scratch pod disproves that: at --ctx-size 65536 with a single slot,
so four times the headroom, the same server answers a 650-token prompt
correctly seven times in a row. What breaks it is how long the prompt is.

    13588 prompt tokens   coherent answer
    15086                 one token, immediate EOS, empty content
    16088                 CJK characters and digit runs

And one request past the boundary poisons the slot: the 13588-token
prompt that had just answered cleanly returns one empty token afterwards
and stays broken until the pod restarts. That explains what looked
intermittent, and why a 650-token classification once came back as 1025
tokens of "!" — it followed Home Assistant's ~19k prompts through the
same slot.

The practical consequence changes with it. The rule is not "keep this
flag low", it is "keep callers under roughly 14k tokens". Paperless sits
near 5k and was never at risk; Home Assistant's Assist prompt measured
18910 and never had a chance, which is a better explanation of its empty
completions and invented device list than the model being weak at long
contexts.

Nothing about the failure is visible from outside: finish_reason "stop",
a well-formed OpenAI response, HTTP 200, /health green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 10:52:04 +02:00
k.renner f7f041e71f Merge pull request 'feat(paperless-eval): run a measurement as one self-contained job' (#776) from paperless-eval-jobs into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#776
2026-09-03 07:51:59 +00:00
claude-botandClaude Opus 5 5ccf9672af chore(paperless-eval): drop a __pycache__ left by a syntax check
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Committed by accident from running py_compile against eval.py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 09:45:03 +02:00
claude-botandClaude Opus 5 55dcd60fa8 feat(paperless-eval): run a measurement as one self-contained job
A paperless instance points at exactly one chat endpoint, so comparing
two candidates means two paperless instances. The pod is therefore the
unit, not the namespace: llama-server, redis and paperless as native
sidecars, the eval script as the one regular container. When it exits the
sidecars are torn down and the Job completes, which a plain multi-
container pod behind a llama-server would not.

Sidecar ordering also removes a failure seen on the smoke test, where
paperless started while the database was still restoring and exited after
its 60s wait.

The script is Python rather than the bash-and-jq version, because the
image has no jq and because the thing that was missing is easier this
way: it writes one row per document with both engines' verdicts. Two
aggregates cannot answer the question worth asking — 74% against 72%
could overlap almost entirely or barely, and that difference is the whole
value of running an LLM beside a classifier that costs 0.6s and keeps
running regardless. The summary now reports how often the LLM rescues a
document the classifier got wrong, and how often it breaks one the
classifier had right.

/data is copied into the pod rather than mounted. LLM_INDEX_DIR is
hardcoded to DATA_DIR/"llm_index", the store behind it is SQLite, and
SQLite locking across pods over CephFS is not dependable — but leaving it
out is worse than sharing it: an empty index makes retrieve_similar_nodes
return nothing, so a job would measure the no-RAG path while reporting a
RAG result. ~160 MiB, seconds, and each job gets a private writable copy.

That volume had to be renamed. paperless-eval-data was RWO and a bound
claim's spec is immutable, so paperless-eval-golden replaces it — named
for its role, since the smoke-test deployment builds it and the jobs read
it.

Anti-affinity puts one measurement per node. Two on a node would share
its iGPU and every timing would then describe contention rather than the
model, which is how 4.7 tokens/s got blamed on context length once
already.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 09:44:51 +02:00
k.renner 92dbb4873b Merge pull request 'fix(paperless-eval): admit the namespace to OpenBao, restore the capabilities' (#775) from paperless-eval-fixes into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#775
2026-09-03 06:55:51 +00:00
claude-botandClaude Opus 5 61bf321b52 fix(paperless-eval): admit the namespace to OpenBao, restore the capabilities
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The instance deployed and got nowhere. Both ExternalSecrets reported
"could not get secret data from provider", so hetzner-s3-creds never
existed, and the CNPG restore job died on it:

    can't get credentials for cluster pprleval-pg: while getting secret
    hetzner-s3-creds: secrets "hetzner-s3-creds" not found

The openbao ClusterSecretStore carries an explicit namespace allowlist in
spec.conditions, and paperless-eval was not on it. Nothing in the
manifests shows that — it is cluster state, which is why neither the
review nor `kubectl kustomize` caught it, and why the failure only
appeared on deploy.

The paperless container also gets production's securityContext back. The
review was right about this and it was dismissed as being about the
scanner and mail bridge sidecars, which those manifests were only cited
as evidence for the pattern. The paperless-ngx entrypoint starts as root,
chowns the mounted volumes and drops to uid 1000 via setgid/setuid, so
drop: ["ALL"] with nothing added fails that sequence with EPERM. It would
have bitten as soon as the restore succeeded and the pod actually tried
to start — and harder here than in production, whose volumes were chowned
once and kept it, while these are fresh on every rebuild.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 08:53:04 +02:00
k.renner a8e981b27a Merge pull request 'feat(paperless-eval): a disposable copy to measure the LLM against' (#774) from paperless-eval-instance into master
Reviewed-on: k.renner/homelab#774
2026-09-03 06:48:54 +00:00
claude-botandClaude Opus 5 b18ed71361 fix(paperless-eval): open the other half of the embedding path
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The egress grant to llama-embed was there; the matching ingress in
ai-ns.yaml was not, so Cilium would have dropped every embedding call —
the one dependency an eval has outside its own pod. The same pairing was
done correctly for home-assistant in #759 and simply missed here.

Scoped to the namespace rather than an app label, unlike the grants
around it. That namespace exists only to run evaluations, and its
workload is jobs created per measurement whose labels vary with what is
being measured; a label match would present later as an embedding call
silently dropped.

Also from the review:

secret.reloader annotation alongside the configmap one, matching
production. The secret refreshes on the same 10m cycle, and without the
annotation a rotated key would sit unused until something else restarted
the pod.

The retention argument in cluster.yaml was too thin, and checking it made
it worse before it made it better: the plugin runs retention from the
instance sidecar on a timer
(instanceSidecarConfiguration.retentionPolicyIntervalSeconds, default
1800), not as part of a backup — so "this cluster never backs up" would
not have covered it.

What does cover it is a second property: retention runs
barman-cloud-backup-delete, which takes a server name and operates inside
<destinationPath>/<serverName>. The production store's status keys its
recovery window by "pprl-pg", confirming that layout; this cluster is
pprleval-pg, so even an accidental retention policy would address an
empty prefix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 08:34:49 +02:00