diff --git a/.github/buildcheck/natives.py b/.github/buildcheck/natives.py index 3baf0736e..fed6dc129 100644 --- a/.github/buildcheck/natives.py +++ b/.github/buildcheck/natives.py @@ -47,6 +47,8 @@ SMOKE_EXCLUDED_OSES = ("Linux-Android",) # The job that re-uploads the Windows natives artifacts Authenticode-signed (CLAUDE.md, "Code signing"). SIGN_JOB = "sign-windows-natives" +# A job-level key sits at the four-space indent of the job body (` if:`); a step's `if:` is deeper. +JOB_CONDITION = re.compile(r"^ if:") SMOKE_EXCLUDED_TREES = ("Linux/s390x",) # `package` uploads each smoke set alone, as llama-smoke-, for the smoke jobs; a matrix row @@ -245,7 +247,14 @@ def check_signing(natives, jobs, uploads): or reads one of them -- `overwrite` deletes the artifact for a moment, so a build or test job still reading it would race (the Windows Java test job reads natives-cpu-windows-x86-64 through the reusable workflow's input). A Windows row without its step would ship that jar unsigned on a - signed run, which `--require-signed` in `package` then reports late.""" + signed run, which `--require-signed` in `package` then reports late. + + And the job carries no job-level `if:`: it must run on every run (a no-op without the switch, + gated step by step) rather than be skipped, because GitHub applies a skip to every job in the + dependency chain behind the skipped one, past any condition of the jobs in between -- the first + form was `if:`-skipped without the switch, `package` tolerated that with `!cancelled()` and ran + green, and the smoke sets, the report and both publish jobs behind `package` were skipped with + the run concluding "success" (run 38870889665, attempt 2).""" windows = [r["classifier"] for r in natives if tree(r).split("/")[0] == "Windows"] sign = jobs.get(SIGN_JOB) if sign is None: @@ -253,6 +262,10 @@ def check_signing(natives, jobs, uploads): signed = {c for c, uploaders in uploads.items() if SIGN_JOB in uploaders} failures = [f"publish.yml {SIGN_JOB} re-uploads (one per Windows row): missing {c}" for c in sorted(set(windows) - signed)] + if any(JOB_CONDITION.match(line) for line in sign.lines): + failures.append(f"publish.yml {SIGN_JOB} carries a job-level `if:` -- the job must run on every run " + f"(a no-op without the switch), never be skipped: a skipped job skips every job behind " + f"`package` in the dependency chain, past any condition in between") failures += [f"publish.yml {SIGN_JOB} re-uploads natives-{c}, which is not a Windows row -- it would replace " f"the build artifact of another platform with an unsigned copy" for c in sorted(signed - set(windows))] diff --git a/.github/buildcheck/tests/test_natives.py b/.github/buildcheck/tests/test_natives.py index f2602f783..e442cc125 100644 --- a/.github/buildcheck/tests/test_natives.py +++ b/.github/buildcheck/tests/test_natives.py @@ -186,6 +186,18 @@ def test_the_sign_job_re_uploads_every_windows_row_with_overwrite(self): "error` would leave the unsigned build artifact in place when the download matched " "nothing"] * len(WINDOWS)) + def test_the_sign_job_carries_no_job_level_condition(self): + """A skipped sign job skips every job behind package (GitHub's dependency-chain rule), so the job + must run on every run; a step-level `if:` (the per-step gate on the switch) is what it uses.""" + skipped = [" if: github.event_name == 'workflow_dispatch' && inputs.sign_windows"] + sign_uploads() + self.assertEqual(natives.check_workflow(ROWS, jobs(sign=skipped)), + ["publish.yml sign-windows-natives carries a job-level `if:` -- the job must run on every " + "run (a no-op without the switch), never be skipped: a skipped job skips every job " + "behind `package` in the dependency chain, past any condition in between"]) + gated = [line for step in sign_uploads() for line in + ([step, " if: env.SIGN_WINDOWS == 'true'"] if step.startswith(" - ") else [step])] + self.assertEqual(natives.check_workflow(ROWS, jobs(sign=gated)), []) + def test_the_sign_job_waits_for_every_reader_of_a_windows_artifact(self): # a job reading the artifact between the build and the signed re-upload would race the overwrite self.assertEqual(natives.check_workflow(ROWS, jobs(consumer_needs="[build]")), diff --git a/.github/smoke-natives-jars.sh b/.github/smoke-natives-jars.sh index 7e9ed2252..c35acaeec 100755 --- a/.github/smoke-natives-jars.sh +++ b/.github/smoke-natives-jars.sh @@ -54,8 +54,18 @@ classes="$(find "$TARGET" -maxdepth 1 -name 'llama-*.jar' | grep -E '/llama-[0-9 mapfile -t natives < <(find "$TARGET" -maxdepth 1 -name 'llama-*.jar' ! -path "$classes" ! -name '*-sources.jar' \ ! -name '*-javadoc.jar' ! -name '*-jar-with-dependencies.jar' | sort) [ "${#natives[@]}" -gt 0 ] || { echo "::error::no natives jars in $TARGET" >&2; exit 2; } +# The classes jar is runnable: its manifest names ServerLauncher, which is what the README's +# `jbang --deps net.ladenthin:llama:` lines rely on (JBang runs a GAV through its +# Main-Class). Read from the jar itself, not from the pom: the entry went missing once, with the +# fat-jar assembly that used to carry it, and nothing noticed. +main_class="$(unzip -p "$classes" META-INF/MANIFEST.MF | tr -d '\r' | sed -n 's/^Main-Class: //p')" +[ "$main_class" = "net.ladenthin.llama.server.ServerLauncher" ] \ + || { echo "::error::the classes jar $classes names '$main_class' as Main-Class, expected net.ladenthin.llama.server.ServerLauncher" >&2; exit 1; } +echo "classes jar: $classes (Main-Class $main_class); ${#natives[@]} natives jars" +# The consumer's classpath: the classes jar, every natives jar and the runtime dependencies. This +# line went missing once (F269 replaced it with the Main-Class check above), and bash -n cannot see +# an unbound variable: the first package job to reach this step after that died on `$path` below. path="$classes:$(IFS=:; echo "${natives[*]}"):$DEPS" -echo "classes jar: $classes; ${#natives[@]} natives jars" run() { local label="$1" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5e49d9728..5dd3d9808 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -42,8 +42,9 @@ env: # dispatch input, for snapshot and release alike -- the pipeline is the same at that point, and # a release is `publish_to_central` + `sign_windows` both on. Never on a push or a pull request, # as nothing publishes from those either. What the switch does: the sign-windows-natives job - # runs (the maven-central environment's eSigner secrets), and package / the publish jobs require - # every Windows DLL of the merged tree to be signed. + # signs (it runs on every run, but only with the switch does it take the maven-central + # environment and its eSigner secrets), and package / the publish jobs require every Windows DLL + # of the merged tree to be signed. SIGN_WINDOWS: ${{ github.event_name == 'workflow_dispatch' && inputs.sign_windows }} SCCACHE_WEBDAV_ENDPOINT: https://cache.depot.dev # The CI model set is .github/models.csv (URLs, and the model cache key is its hash). The Java @@ -2232,9 +2233,16 @@ jobs: build-linux-x86_64-sycl: name: Build Linux x86_64 SYCL (Intel oneAPI, fp16) needs: [startgate, build-webui] - runs-on: ubuntu-latest + # Pinned to 24.04 like upstream's ubuntu-24-sycl: the Level Zero debs below are the u24.04 ones. + runs-on: ubuntu-24.04 env: SCCACHE_WEBDAV_TOKEN: ${{ secrets.DEPOT_TOKEN }} + # KEEP IN SYNC WITH UPSTREAM (release.yml, ubuntu-24-sycl): installer URLs and the Level Zero + # release; see the install steps below. + ONEAPI_DLE_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/b5c100a7-8052-4e88-90e2-63d9916a998b/intel-deep-learning-essentials-2026.1.4.17_offline.sh + ONEAPI_ONEDNN_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/94c3dbac-0852-45be-a57d-21c204cada3e/intel-onednn-2026.0.2.46_offline.sh + LEVEL_ZERO_VERSION: "1.33.1" + LEVEL_ZERO_UBUNTU_VERSION: "u24.04" steps: - name: Free up disk space # The GPU toolkit this job installs runs to several GB on top of the llama.cpp build tree; @@ -2251,18 +2259,52 @@ jobs: with: distribution: 'temurin' java-version-file: .java-version - - name: Install Intel oneAPI (DPC++ + MKL + oneDNN) + - name: Install Intel oneAPI (Deep Learning Essentials + oneDNN, pinned offline installers) + # KEEP IN SYNC WITH UPSTREAM: the two installer URLs are those of llama.cpp's ubuntu-24-sycl + # release job (.github/workflows/release.yml at the pinned tag; b11654/#30131 replaced the + # oneAPI Base Toolkit by the Deep Learning Essentials bundle -- DPC++, MKL, TBB -- plus a + # separate oneDNN installer, which the bundle no longer carries). Until then this job took + # the same three components from Intel's apt repository, unpinned (whatever the repository + # served that day); the offline installers name a version, which is what the hard-pinning + # policy (TODO.md, delivery roadmap item 3) asks for. Intel publishes no checksum next to + # the files -- the same gap the Windows installer has, recorded there. + # # oneDNN is required since llama.cpp #29107 (b11636): GGML_SYCL_DNN defaults to ON and - # ggml-sycl's CMakeLists now stops with FATAL_ERROR when DNNL is missing (it used to log + # ggml-sycl's CMakeLists stops with FATAL_ERROR when DNNL is missing (it used to log # "oneDNN not found" and build a module whose flash attention runs the slower MKL path). - # intel-oneapi-dnnl-devel is the oneAPI-bundled GPU build the error asks for -- not - # Ubuntu's CPU-only libdnnl-dev, which the same change rejects -- and setvars.sh puts it on - # CMAKE_PREFIX_PATH. The Windows SYCL job installs the matching intel.oneapi.win.dnnl. + # Intel's own oneDNN is the GPU build the error asks for -- not Ubuntu's CPU-only + # libdnnl-dev, which the same change rejects -- and setvars.sh puts it on CMAKE_PREFIX_PATH. run: | - wget -qO- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor | sudo tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null - echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" | sudo tee /etc/apt/sources.list.d/oneAPI.list + set -euo pipefail + cd "${RUNNER_TEMP}" + echo "Download oneAPI Deep Learning Essentials installation package" + wget -nv "${ONEAPI_DLE_URL}" -O intel-deep-learning-essentials_offline.sh + echo "Install oneAPI Deep Learning Essentials components" + sudo bash intel-deep-learning-essentials_offline.sh -s -a --silent --eula accept + echo "Download oneDNN installation package" + wget -nv "${ONEAPI_ONEDNN_URL}" -O intel-onednn_offline.sh + echo "Install oneDNN components" + sudo bash intel-onednn_offline.sh -s -a --silent --eula accept + rm -f intel-deep-learning-essentials_offline.sh intel-onednn_offline.sh + test -f /opt/intel/oneapi/setvars.sh + - name: Install the Level Zero loader and headers + # As upstream's ubuntu-24-sycl job (the Level Zero release debs, version LEVEL_ZERO_VERSION + # above; upstream has had the step since #21597 / #29273, long before the installer move): + # ggml-sycl's GGML_SYCL_SUPPORT_LEVEL_ZERO_API (default ON) links libze_loader when the loader + # and level_zero/ze_api.h are found, for direct device allocations on multi-GPU hosts + # (llama.cpp #21597); without them it only warns and builds the module without that path. + # THIS IS NEW FOR THE SHIPPED MODULE: the module built from the apt packages never found a + # loader (the package job of run 38870889665 listed no libze_loader among its imports), so + # from here on libggml-sycl.so imports libze_loader.so.1 -- the Intel GPU driver's loader -- + # and carries the Level Zero allocation path, as upstream's own release module does. + # apt-get update first: the old oneAPI step ran it, and the patchelf install below relied on it. + run: | + set -euo pipefail + cd "${RUNNER_TEMP}" sudo apt-get update - sudo apt-get install -y intel-oneapi-compiler-dpcpp-cpp intel-oneapi-mkl-devel intel-oneapi-dnnl-devel + wget -nv "https://github.com/oneapi-src/level-zero/releases/download/v${LEVEL_ZERO_VERSION}/libze1_${LEVEL_ZERO_VERSION}%2B${LEVEL_ZERO_UBUNTU_VERSION}_amd64.deb" -O libze1.deb + wget -nv "https://github.com/oneapi-src/level-zero/releases/download/v${LEVEL_ZERO_VERSION}/libze-dev_${LEVEL_ZERO_VERSION}%2B${LEVEL_ZERO_UBUNTU_VERSION}_amd64.deb" -O libze-dev.deb + sudo apt-get install -y ./libze1.deb ./libze-dev.deb - name: Build libraries shell: bash run: | @@ -2318,6 +2360,10 @@ jobs: runs-on: windows-2025-vs2026 env: SCCACHE_WEBDAV_TOKEN: ${{ secrets.DEPOT_TOKEN }} + # KEEP IN SYNC WITH UPSTREAM (release.yml, windows-sycl): see the install step below. + ONEAPI_DLE_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/a51104eb-6f4a-4a0e-9dae-1b81b0c81ae1/intel-deep-learning-essentials-2026.1.4.16_offline.exe + ONEAPI_ONEDNN_URL: https://registrationcenter-download.intel.com/akdlm/IRC_NAS/34e28083-d04b-46a3-826c-8c1d00bf1e1c/intel-onednn-2026.0.2.47_offline.exe + LEVEL_ZERO_SDK_URL: https://github.com/oneapi-src/level-zero/releases/download/v1.33.1/level-zero-win-sdk-1.33.1.zip steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - name: Download shared WebUI assets @@ -2329,14 +2375,47 @@ jobs: uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1 with: arch: x64 - - name: Install Intel oneAPI (DPC++ + MKL + oneDNN + TBB) + - name: Install Intel oneAPI (Deep Learning Essentials + oneDNN, pinned offline installers) shell: cmd - # Mirrors upstream llama.cpp's windows-sycl release job: extract the offline - # installer, then run its bootstrapper with the DPC++/MKL/oneDNN/TBB components. + # KEEP IN SYNC WITH UPSTREAM: the two installers and their component lists are those of + # llama.cpp's windows-sycl release job (release.yml at the pinned tag, scripts/install-oneapi.bat; + # b11654/#30131). Deep Learning Essentials 2026.1 carries DPC++, MKL and TBB; oneDNN is a + # separate installer since then (the 2025.3.3 bundle this job used before had it as a + # component). Each: extract the offline installer, run its bootstrapper with the components. + # A cmd step's result is the exit code of its LAST line, so every download, extraction and + # bootstrapper carries `|| exit /b 1` -- without it a failed installer would be hidden behind + # the clean-up lines and surface two steps later as a missing compiler -- and the clean-up + # runs after the setvars.bat check with a final `exit /b 0`, so a locked file cannot fail a + # successful install either. The curls retry like upstream's script; the three VS integration + # flags are upstream's. Intel publishes no checksum next to the files (TODO.md, roadmap item 3). + # THIS CHANGES THE SHIPPED MODULE'S RUNTIME: Deep Learning Essentials 2026.1 is the oneAPI + # 2026 runtime (sycl9.dll, mkl_sycl_blas.6.dll; the 2025.3 bundle gave sycl8.dll and + # mkl_sycl_blas.5.dll), and with the Level Zero SDK below the module imports ze_loader.dll, + # which it did not before (run 38870889665). A consumer with the 2025.3 runtime on PATH loses + # the module silently, because ggml skips a module that fails to load (README, "sycl-windows"). run: | - curl -fSL -o "%RUNNER_TEMP%\oneapi.exe" "https://registrationcenter-download.intel.com/akdlm/IRC_NAS/b60765d1-2b85-4e85-86b6-cb0e9563a699/intel-deep-learning-essentials-2025.3.3.18_offline.exe" - "%RUNNER_TEMP%\oneapi.exe" -s -x -f "%RUNNER_TEMP%\oneapi_extracted" --log "%RUNNER_TEMP%\extract.log" - "%RUNNER_TEMP%\oneapi_extracted\bootstrapper.exe" -s --action install --components=intel.oneapi.win.cpp-dpcpp-common:intel.oneapi.win.mkl.devel:intel.oneapi.win.dnnl:intel.oneapi.win.tbb.devel --eula=accept -p=NEED_VS2022_INTEGRATION=0 --log-dir="%RUNNER_TEMP%" + curl -fSL --retry 5 --retry-delay 5 -o "%RUNNER_TEMP%\oneapi.exe" "%ONEAPI_DLE_URL%" || exit /b 1 + "%RUNNER_TEMP%\oneapi.exe" -s -x -f "%RUNNER_TEMP%\oneapi_extracted" --log "%RUNNER_TEMP%\extract.log" || exit /b 1 + "%RUNNER_TEMP%\oneapi_extracted\bootstrapper.exe" -s --action install --components=intel.oneapi.win.cpp-dpcpp-common:intel.oneapi.win.mkl.devel:intel.oneapi.win.tbb.devel --eula=accept -p=NEED_VS2017_INTEGRATION=0 -p=NEED_VS2019_INTEGRATION=0 -p=NEED_VS2022_INTEGRATION=0 --log-dir="%RUNNER_TEMP%" || exit /b 1 + curl -fSL --retry 5 --retry-delay 5 -o "%RUNNER_TEMP%\onednn.exe" "%ONEAPI_ONEDNN_URL%" || exit /b 1 + "%RUNNER_TEMP%\onednn.exe" -s -x -f "%RUNNER_TEMP%\onednn_extracted" --log "%RUNNER_TEMP%\extract-onednn.log" || exit /b 1 + "%RUNNER_TEMP%\onednn_extracted\bootstrapper.exe" -s --action install --components=intel.oneapi.win.dnnl --eula=accept -p=NEED_VS2017_INTEGRATION=0 -p=NEED_VS2019_INTEGRATION=0 -p=NEED_VS2022_INTEGRATION=0 --log-dir="%RUNNER_TEMP%" || exit /b 1 + if not exist "C:\Program Files (x86)\Intel\oneAPI\setvars.bat" exit /b 1 + rmdir /s /q "%RUNNER_TEMP%\oneapi_extracted" + rmdir /s /q "%RUNNER_TEMP%\onednn_extracted" + del "%RUNNER_TEMP%\oneapi.exe" "%RUNNER_TEMP%\onednn.exe" + exit /b 0 + - name: Install the Level Zero SDK + # As upstream's windows-sycl job (there since #21597 / #29273): ggml-sycl reads + # LEVEL_ZERO_V1_SDK_PATH for ze_loader.lib and level_zero/ze_api.h + # (GGML_SYCL_SUPPORT_LEVEL_ZERO_API, default ON; direct device allocations on multi-GPU + # hosts) and only warns without them. New import of the shipped module (see the step above): + # ze_loader.dll comes with Intel's graphics driver at run time; nothing of the SDK ships. + shell: pwsh + run: | + Invoke-WebRequest -Uri "${{ env.LEVEL_ZERO_SDK_URL }}" -OutFile "$env:RUNNER_TEMP\level-zero-win-sdk.zip" + Expand-Archive -Path "$env:RUNNER_TEMP\level-zero-win-sdk.zip" -DestinationPath "C:/level-zero-sdk" -Force + "LEVEL_ZERO_V1_SDK_PATH=C:/level-zero-sdk" | Out-File -FilePath $env:GITHUB_ENV -Append - name: Install sccache (shared compiler cache) if: env.USE_CACHE == 'true' && env.SCCACHE_WEBDAV_TOKEN != '' continue-on-error: true @@ -2904,16 +2983,28 @@ jobs: # --------------------------------------------------------------------------- # Windows code signature (CLAUDE.md, "Code signing of the Windows DLLs"; TODO.md, delivery - # roadmap, item 11). Only on a dispatch with `sign_windows`: Authenticode over every DLL of - # every Windows natives artifact with SSL.com eSigner through jsign, then the artifacts are - # re-uploaded under their own names (`overwrite: true`), so `package` and the publish jobs - # merge signed DLLs, the Windows smoke rows run the signed sets and the jars carry them. + # roadmap, item 11). Signing happens only on a dispatch with `sign_windows`: Authenticode over + # every DLL of every Windows natives artifact with SSL.com eSigner through jsign, then the + # artifacts are re-uploaded under their own names (`overwrite: true`), so `package` and the + # publish jobs merge signed DLLs, the Windows smoke rows run the signed sets and the jars carry + # them. # # A job of its own, not steps in `package`: the eSigner secrets live in the maven-central # environment (the GPG key's), and `package` runs on every pull request, where that # environment's rules must not apply. The TOTP secret lets whoever holds it sign in the owner's - # name, so it is handed to exactly one job, which runs only on the explicit switch and only - # where the environment allows. The job waits for every job that reads a Windows natives + # name, so it is handed to exactly one job, which signs only on the explicit switch and only + # where the environment allows. + # + # The job itself runs on EVERY run and is never skipped -- without the switch it does nothing + # (every step but the first is gated on SIGN_WINDOWS) and takes the unprotected environment + # `unsigned`, which holds no secrets. It cannot be `if:`-skipped: GitHub applies a skip "to all + # jobs in the dependency chain from the point of failure or skip onwards", through every job in + # between whatever its own condition says, so a skipped sign job skipped the smoke sets, the + # report and both publish jobs behind `package` even though `package` itself tolerated the + # skip and ran green (run 38870889665 attempt 2, the first run to reach `package` after the + # job existed). The environment name is an expression for that reason: `maven-central` only + # when signing, so its protection rules never apply to a pull request. + # The job waits for every job that reads a Windows natives # artifact (the Windows Java test job downloads natives-cpu-windows-x86-64): `overwrite` # deletes the artifact for a moment, and a download in that moment would fail. # check-natives.py holds the upload steps to the Windows rows of natives.csv and the `needs` @@ -2922,26 +3013,32 @@ jobs: sign-windows-natives: name: Sign the Windows DLLs (SSL.com eSigner) needs: [build-windows-x86_64, build-windows-arm64, build-windows-x86_64-cuda, build-windows-x86_64-vulkan, build-windows-x86_64-opencl, build-windows-x86_64-rocm, build-windows-x86_64-sycl, build-windows-arm64-opencl, build-windows-arm64-vulkan, build-windows-x86_64-openvino, test-java-windows-x86_64] - if: github.event_name == 'workflow_dispatch' && inputs.sign_windows runs-on: ubuntu-latest - environment: maven-central + environment: ${{ github.event_name == 'workflow_dispatch' && inputs.sign_windows && 'maven-central' || 'unsigned' }} permissions: contents: read steps: + - name: Nothing to sign (no sign_windows switch) -- the build artifacts stay as uploaded + if: env.SIGN_WINDOWS != 'true' + run: echo "sign_windows is off; the Windows natives artifacts are re-used unsigned (jllama-build.txt says signed=no)." - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + if: env.SIGN_WINDOWS == 'true' with: persist-credentials: false - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6 + if: env.SIGN_WINDOWS == 'true' with: distribution: 'temurin' java-version-file: .java-version # Unmerged, one directory per artifact (Windows/// inside), the layout the # build jobs uploaded and merge-native-artifacts.sh expects back. - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 + if: env.SIGN_WINDOWS == 'true' with: pattern: "natives-*-windows-*" path: ${{ github.workspace }}/native-artifacts/ - name: Sign every Windows DLL and mark the directories signed=yes + if: env.SIGN_WINDOWS == 'true' env: ES_USERNAME: ${{ secrets.ES_USERNAME }} ES_PASSWORD: ${{ secrets.ES_PASSWORD }} @@ -2952,63 +3049,74 @@ jobs: # Every DLL of every directory carries a signature now, read from the file (the attribute # certificate table), before anything is re-uploaded. - name: Verify the signatures + if: env.SIGN_WINDOWS == 'true' run: python3 .github/verify-native-deps.py --require-signed "${{ github.workspace }}/native-artifacts" # One upload per artifact, same name, same layout; overwrite replaces the build job's. - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + if: env.SIGN_WINDOWS == 'true' with: name: natives-cpu-windows-x86-64 path: ${{ github.workspace }}/native-artifacts/natives-cpu-windows-x86-64/ overwrite: true if-no-files-found: error - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + if: env.SIGN_WINDOWS == 'true' with: name: natives-cpu-windows-aarch64 path: ${{ github.workspace }}/native-artifacts/natives-cpu-windows-aarch64/ overwrite: true if-no-files-found: error - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + if: env.SIGN_WINDOWS == 'true' with: name: natives-cuda13-windows-x86-64 path: ${{ github.workspace }}/native-artifacts/natives-cuda13-windows-x86-64/ overwrite: true if-no-files-found: error - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + if: env.SIGN_WINDOWS == 'true' with: name: natives-vulkan-windows-x86-64 path: ${{ github.workspace }}/native-artifacts/natives-vulkan-windows-x86-64/ overwrite: true if-no-files-found: error - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + if: env.SIGN_WINDOWS == 'true' with: name: natives-opencl-windows-x86-64 path: ${{ github.workspace }}/native-artifacts/natives-opencl-windows-x86-64/ overwrite: true if-no-files-found: error - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + if: env.SIGN_WINDOWS == 'true' with: name: natives-rocm-windows-x86-64 path: ${{ github.workspace }}/native-artifacts/natives-rocm-windows-x86-64/ overwrite: true if-no-files-found: error - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + if: env.SIGN_WINDOWS == 'true' with: name: natives-sycl-windows-x86-64 path: ${{ github.workspace }}/native-artifacts/natives-sycl-windows-x86-64/ overwrite: true if-no-files-found: error - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + if: env.SIGN_WINDOWS == 'true' with: name: natives-opencl-windows-aarch64 path: ${{ github.workspace }}/native-artifacts/natives-opencl-windows-aarch64/ overwrite: true if-no-files-found: error - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + if: env.SIGN_WINDOWS == 'true' with: name: natives-vulkan-windows-aarch64 path: ${{ github.workspace }}/native-artifacts/natives-vulkan-windows-aarch64/ overwrite: true if-no-files-found: error - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 + if: env.SIGN_WINDOWS == 'true' with: name: natives-openvino-windows-x86-64 path: ${{ github.workspace }}/native-artifacts/natives-openvino-windows-x86-64/ @@ -3054,14 +3162,12 @@ jobs: - test-java-macos-arm64-metal-15 - test-java-windows-x86_64 - test-java-windows-x86_64-msvc + # sign-windows-natives runs on every run (a no-op without the `sign_windows` switch), so the + # default condition is right here: a sign job that ran and failed -- wrong secrets, the + # environment refusing the branch -- skips this job, because the signature was asked for. + # It must never be a skipped job: a skip propagates down the whole dependency chain past + # any `if:` in between (see the comment on that job). - sign-windows-natives - # sign-windows-natives is skipped on every run without the `sign_windows` switch, and a skipped - # `needs` entry skips the dependents unless the condition says otherwise: run on success of - # everything else, never after a failure or a cancellation (every other entry is an - # unconditional job, so "not failed, not cancelled" is "succeeded" for them). A sign job that - # ran and failed -- wrong secrets, the environment refusing the branch -- skips this job: the - # signature was asked for. - if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} runs-on: ubuntu-latest # id-token + attestations: the build-provenance attestation below (Sigstore) signs a statement # that these exact files came out of this workflow at this commit; contents stays read. diff --git a/BUGHUNT-CHECKED.md b/BUGHUNT-CHECKED.md new file mode 100644 index 000000000..7286c5a5f --- /dev/null +++ b/BUGHUNT-CHECKED.md @@ -0,0 +1,186 @@ + + + +# Bug-Hunt: geprüfte Stellen — java-llama.cpp + +Jede Zeile unter „Geprüft“ steht für eine gründlich gelesene Produktionsdatei, **genau eine Zeile je Pfad**: +`Pfad` · Blob-Hash der Datei beim Prüfen · Datum der letzten Lesung · Ergebnis — Notizen. Eine Datei gilt als +geprüft, solange `git rev-parse --short HEAD:` denselben Blob-Hash liefert. Ändert sich die Datei, wandert +ihre Zeile nach „Abgelaufen – erneut zu prüfen“, mit dem Blob am HEAD und den Fixes, die sie seitdem geändert +haben; zurück unter „Geprüft“ kommt sie erst mit einer neuen vollständigen Lesung und dem dann gelesenen Blob. +Eine Zeile wird nie umdatiert oder auf einen neuen Blob gesetzt, ohne die Datei erneut gelesen zu haben (ein +geprüfter Fix-Diff ist keine Lesung der Klasse). Ergebnisse: `kein Fund`, `widerlegt` (Verdacht geprüft, Code +korrekt), `unsicher` (offen, siehe [`BUGHUNT-FOUND.md`](BUGHUNT-FOUND.md)), `Fund Fnn` (Fehler gefunden und dort +beschrieben). Mehrere Durchsichten derselben Datei am selben Blob stehen in einer Zeile, getrennt durch `‖`. + +Bis 2026-10-20 wurde je Durchsicht eine Zeile angehängt (274 Zeilen für 124 Pfade, von denen am 2026-10-21 +43 abgelaufen waren); am 2026-10-21 wurde die Datei auf eine Zeile je Pfad zusammengeführt, ohne eine Datei neu +zu lesen. Prüfung, ob eine Zeile unter „Geprüft“ abgelaufen ist (gibt nichts aus, solange alle stimmen): + +```bash +sed -n '/^## Geprüft/,/^## Abgelaufen/p' BUGHUNT-CHECKED.md | sed -n 's/^- `\([^`]*\)` · `\([0-9a-f]*\)`.*/\1 \2/p' | + while read -r p h; do c=$(git rev-parse HEAD:"$p"); case "$c" in "$h"*) ;; *) echo "abgelaufen: $p ($h, HEAD ${c:0:7})";; esac; done +``` + +## Geprüft + +- `llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmTaskStream.java` · `11dd00a` · 2026-10-18 · kein Fund, kein Fund (nur gelesen, kein JDK 22 hier) — (2026-10-17, kein Fund) Durchsicht nach dem F36-Muster: `finished` wird nie wieder auf `false` gesetzt, `close()` gibt die Task genau einmal frei, auch nach dem natürlichen Stopp; kein Versprechen für Nutzung aus mehreren Threads (Felder nicht `volatile`, im Vertrag nicht verlangt). Grep nach `= !…stop` in allen Java-Zielen: kein weiterer Treffer. ‖ (2026-10-18, kein Fund (nur gelesen, kein JDK 22 hier)) Durchsicht „`FfmTaskStream.java` + `FfmLlamaModel.java` (`receive`, `releaseTask`, `close`) ↔ `jllama_api.h` (Schnittstelle Streaming-Aufgabe über FFM)“: Nach `stop` gibt `next()` die Aufgabe genau einmal frei (`released`), ein frühes `close()` ebenso; wirft der Empfang (Fehlerergebnis), hat die Engine den Reader schon entfernt, ein späteres `close()` trifft eine unbekannte Id, die laut Header ignoriert wird. Nach `model.close()` ruft `releaseTask` nichts mehr auf. Das Prüfen-dann-Aufrufen von `handle()` gegen ein paralleles `close()` ist in der Klassen-Javadoc als Vertrag festgehalten (siehe F59); deren Vergleich „as with `LlamaModel`“ stimmt nur halb, denn das JNI-`LlamaModel` liest den Handle unter `g_ctx_mutex` und übersteht ein paralleles `close()`. +- `llama-ffm/src/main/java/net/ladenthin/llama/ffm/Jllama.java` · `1c19474` · 2026-10-17 · kein Fund — java-llama.cpp `llama-langchain4j` (`StreamingChunkAssembler`, `LangChain4jMapping`, `Jllama*Model`). **Lücke (niedrig, nicht geändert):** Der nicht-streamende `JllamaChatModel` übernimmt `reasoning_content` nicht nach `AiMessage.thinking()`, der streamende tut es – dasselbe Modell liefert also j… +- `llama-ffm/src/main/java/net/ladenthin/llama/ffm/SlotOp.java` · `f6fdfd1` · 2026-10-17 · kein Fund — Codes 1/2/3 stimmen mit `jllama_slot_op` in `jllama_api.h` überein. +- `llama-kotlin/src/main/kotlin/net/ladenthin/llama/kotlin/LlamaFlows.kt` · `45f5f14` · 2026-10-18 · kein Fund, kein Fund (Doku-Abweichung notiert) — (2026-10-17, kein Fund) `closeableIterableFlow` öffnet die Quelle je Sammlung und schließt sie über `use` bei Ende, Fehler und Abbruch (ein `emit` nach Abbruch wirft `CancellationException`, `use` schließt). Abbruch wirkt erst nach Rückkehr des blockierenden `next()`, wie die KDoc mit dem Hinweis auf `flowOn(Dispatchers.IO)` sagt. ‖ (2026-10-17, kein Fund (Doku-Abweichung notiert)) Durchsicht „`LlamaSuspend.kt` + `LlamaFlows.kt` ↔ `LlamaModel.complete(…, CancellationToken)` / `LlamaIterable` / `jllama_engine::stream_next` (Schnittstelle Kotlin-Abbruch ↔ native Aufgabe)“: Abbruch der Koroutine setzt das Token, `complete` prüft es vor jedem `receiveCompletionJson` und gibt die Aufgabe per `cancelCompletion` frei; nach `stop` und bei jedem Fehler entfernt `stream_next` den Reader selbst; `coroutineScope` wartet auf den blockierenden `async`-Block, keine laufende Generierung überlebt den Aufruf. Flows schließen die Quelle über `use` bei Ende, Fehler und Abbruch. `CancellationToken.reset()` ist `public`, die Javadoc nennt es „Package-private“ (reine Doku, kein Test möglich, nicht geändert). ‖ (2026-10-18, kein Fund) `use` schließt die Quelle bei Ende, Fehler und Abbruch (`take(n)`); Blockieren des Sammel-Dispatchers ist dokumentiert +- `llama-kotlin/src/main/kotlin/net/ladenthin/llama/kotlin/LlamaSuspend.kt` · `0ff46cf` · 2026-10-18 · kein Fund, kein Fund (Doku-Abweichung notiert) — (2026-10-17, kein Fund) `withCancellationToken`: Abbruch des Aufrufers lässt `await()` mit `CancellationException` enden, das Token wird vor dem Weiterwerfen abgebrochen, und `coroutineScope` wartet, bis der blockierende Block zurückkehrt – keine weiterlaufende Generierung nach der Rückkehr. Abbruch vor dem Start des `async`: Block läuft gar nicht. Fehler im Block brechen den Scope ab und kommen über `await()` beim Aufrufer an. `chatSuspend`/`chatCompleteTextSuspend`/`embedSuspend` dokumentieren ehrlich, dass sie nicht token-abbrechbar sind. Musterdurchsicht nach F45 (inklusive/exklusive Grenzen, Meldung vs. Prüfung) über alle drei Ziele: kein weiterer Treffer. ‖ (2026-10-17, kein Fund (Doku-Abweichung notiert)) Durchsicht „`LlamaSuspend.kt` + `LlamaFlows.kt` ↔ `LlamaModel.complete(…, CancellationToken)` / `LlamaIterable` / `jllama_engine::stream_next` (Schnittstelle Kotlin-Abbruch ↔ native Aufgabe)“: Abbruch der Koroutine setzt das Token, `complete` prüft es vor jedem `receiveCompletionJson` und gibt die Aufgabe per `cancelCompletion` frei; nach `stop` und bei jedem Fehler entfernt `stream_next` den Reader selbst; `coroutineScope` wartet auf den blockierenden `async`-Block, keine laufende Generierung überlebt den Aufruf. Flows schließen die Quelle über `use` bei Ende, Fehler und Abbruch. `CancellationToken.reset()` ist `public`, die Javadoc nennt es „Package-private“ (reine Doku, kein Test möglich, nicht geändert). ‖ (2026-10-18, kein Fund) Durchsicht „`LlamaSuspend.kt` (↔ `LlamaModel.complete(params, token)`)“: Abbruch der Koroutine setzt das Token, `coroutineScope` wartet auf das Kind, `complete` gibt die Aufgabe an der nächsten Token-Grenze frei; vorab abgebrochenes Token gibt sofort frei; Javadoc „chat ohne Token-Überladung“ stimmt +- `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/JllamaChatModel.java` · `62e0ac1` · 2026-10-17 · kein Fund — (2026-10-17, kein Fund) java-llama.cpp `llama-langchain4j` (`StreamingChunkAssembler`, `LangChain4jMapping`, `Jllama*Model`). **Lücke (niedrig, nicht geändert):** Der nicht-streamende `JllamaChatModel` übernimmt `reasoning_content` nicht nach `AiMessage.thinking()`, der streamende tut es – dasselbe Modell liefert also j… ‖ (2026-10-17, kein Fund) Durchsicht „`LangChain4jMapping.java` + `JllamaChatModel.java` + `JllamaStreamingChatModel.java` ↔ `LlamaModel.chat`/`streamChatCompletion` (Schnittstelle LangChain4j-Request → `InferenceParameters`)“: `temperature`/`topP`/`topK`/`maxOutputTokens`(→`n_predict`)/Penalties/`stopSequences` nur bei gesetztem Wert; JSON-`responseFormat` mit Schema → `json_schema`, ohne → `json_object`; Streaming nutzt dieselbe Verdrahtung wie `LlamaModel.chat` (`tool_choice` nur zusammen mit Tools, wie dort). Beide Modelle überschreiben nur `doChat`, so bleiben das Zusammenführen der `defaultRequestParameters` und die Listener von LangChain4j wirksam; `modelName` wird ignoriert (dokumentiert). +- `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/JllamaEmbeddingModel.java` · `9a4b965` · 2026-10-18 · kein Fund — erneut gründlich: ein `embed` je Segment in Eingabereihenfolge; leere Segmente lässt langchain4j nicht zu, `dimension()` per Standard über ein Probe-Embedding. +- `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/JllamaScoringModel.java` · `37473c2` · 2026-10-19 · kein Fund — (2026-10-18, kein Fund) erneut gründlich: Scores per `index` der nativen Antwort in Eingabereihenfolge (`parseRerankScores`, fehlende Indizes 0.0, Rückfall auf Array-Reihenfolge), doppelte Dokumente bleiben getrennt; eine leere Liste postet keine Aufgabe (`jllama_engine.cpp` `rerank`). ‖ (2026-10-18, kein Fund) Durchsicht „`JllamaScoringModel.java` + `LangChain4jMapping.parseRerankScores` ↔ `LlamaModel.handleRerank`“: Schnittstelle (2.3): `rerank_results_to_json` liefert je Ergebnis `index` und lehnt fehlende/ungültige ab, die Rückfall-Zuordnung nach Arrayposition greift also nie; Scores werden nach `index` einsortiert, doppelte Dokumente (anders als in `LlamaModel.rerank(String, String...)`, das nach Text in eine Map schreibt) gehen hier nicht verloren. `JllamaEmbeddingModel` (`9a4b965`): ein `embed` je Segment, Reihenfolge erhalten. ‖ (2026-10-19, kein Fund) Durchsicht „`JllamaScoringModel.java` + `LangChain4jMapping.parseRerankScores`“: Werte werden über `index` (Eingabeposition) zurückgeordnet, ohne `index` über die Reihenfolge; außerhalb liegende Indizes verworfen, doppelte Dokumente bleiben getrennt. Nicht verfolgt: `scoreAll` mit leerer Liste reicht ein leeres Dokument-Array an `handleRerank` weiter (native Seite, ohne Bibliothek nicht messbar). +- `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/JllamaStreamingChatModel.java` · `8cecaa4` · 2026-10-18 · kein Fund — (2026-10-17, kein Fund) Durchsicht „`LangChain4jMapping.java` + `JllamaChatModel.java` + `JllamaStreamingChatModel.java` ↔ `LlamaModel.chat`/`streamChatCompletion` (Schnittstelle LangChain4j-Request → `InferenceParameters`)“: `temperature`/`topP`/`topK`/`maxOutputTokens`(→`n_predict`)/Penalties/`stopSequences` nur bei gesetztem Wert; JSON-`responseFormat` mit Schema → `json_schema`, ohne → `json_object`; Streaming nutzt dieselbe Verdrahtung wie `LlamaModel.chat` (`tool_choice` nur zusammen mit Tools, wie dort). Beide Modelle überschreiben nur `doChat`, so bleiben das Zusammenführen der `defaultRequestParameters` und die Listener von LangChain4j wirksam; `modelName` wird ignoriert (dokumentiert). ‖ (2026-10-18, kein Fund) erneut gründlich: `doChat` reicht jede Exception aus dem Stream an `onError`, `complete()` erst danach; Fehler im Assembler → F128. ‖ (2026-10-18, kein Fund) erneut gründlich: `doChat` reicht jede Exception aus dem Stream an `onError`, `complete()` erst danach; Fehler im Assembler → F128. +- `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/JsonSchemaElementSerializer.java` · `6399310` · 2026-10-18 · kein Fund — (2026-10-17, kein Fund) java-llama.cpp `llama-langchain4j/…/JsonSchemaElementSerializer`: alle 11 `JsonSchemaElement`-Typen abgedeckt, `$ref` → `#/$defs/` passend zu den unter `$defs` geschriebenen Definitionen, Enum als String mit `enum`-Liste wie bei langchain4j. Randnotiz (niedrig): `JsonRawSchema` wird mit `re… ‖ (2026-10-18, kein Fund) Durchsicht „`JsonSchemaElementSerializer.java` (Typ-Weiche gegen langchain4j-core 1.22.0)“: alle elf Elementtypen haben einen Zweig, ein unbekannter wirft (kein stilles Verwerfen); `$defs`/`#/$defs/` wie langchain4js interne Konvention, `required`/`additionalProperties` nur wenn gesetzt. +- `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java` · `e054789` · 2026-10-18 · Fund F94, kein Fund, kein Fund (Anmerkung) — (2026-10-18, Fund F94) Textteile ohne Trenner verbunden; Assistent mit `thinking()` im Verlauf wird ohne Denkinhalt gesendet (wie LangChain4js OpenAI-Adapter ohne `sendThinking`, kein Fund) ‖ (2026-10-18, kein Fund) Durchsicht „`LangChain4jMapping.java` (`toFinishReason`) + `StreamingChunkAssembler.java` (`finish_reason`/Usage im Stream)“: `stop`/`length`/`tool_calls`/`content_filter` auf die passenden Enum-Werte, fehlend → `STOP`, Unbekanntes → `OTHER`; im Stream wird `finish_reason` nur übernommen, wenn es ein String ist (JSON-`null` der Zwischen-Chunks überschreibt nichts), Usage aus dem Chunk mit leeren `choices` ‖ (2026-10-18, kein Fund) Durchsicht „`LangChain4jMapping.java` (`toJllamaRequest`, `applySampling`, `applyResponseFormat`, `toToolChoiceString`)“: `temperature`/`topP`/`topK`/`maxOutputTokens` (→ `n_predict`)/`frequencyPenalty`/`presencePenalty`/`stopSequences` übernommen, leere Stoppliste ausgelassen; JSON mit Schema → `json_schema`, ohne → `json_object`, Text-Format unverändert; `ToolChoice` `REQUIRED`/`NONE`/`AUTO` als Zeichenkette; blockierender und gestreamter Pfad nutzen dieselbe Abbildung ‖ (2026-10-18, kein Fund (Anmerkung)) Durchsicht „`LangChain4jMapping.java` (`toJllamaMessage`, Nachrichtentypen)“: System, User (ein Text, mehrere Texte mit `\n` seit F94, gemischte Teile als Teilliste), Assistent mit/ohne Werkzeugaufrufen (fehlende Id → `""`, fehlende Argumente → `{}`), Werkzeugergebnis mit `tool_call_id`; der Werkzeugname des Ergebnisses fällt weg (`ChatMessage.toolResult` kennt kein `name`), die Id genügt aber zur Zuordnung – anders als bei F116, wo Ollama keine Id schickt; `AiMessage.thinking()` wird nicht zurückgeschickt, wie langchain4js eigener OpenAI-Adapter es ohne `sendThinking` hält ‖ Frühere Durchsichten anderer Blobs: kein Fund (`550a6c2`, 2026-10-17); Fund F62 (nur beschrieben) (`550a6c2`, 2026-10-17); kein Fund (`550a6c2`, 2026-10-17); kein Fund (`550a6c2`, 2026-10-18); Fund F92 (`195bf32`, 2026-10-18); Fund F93 (`832ca3e`, 2026-10-18); kein Fund (`ohne Hash`, 2026-10-18); kein Fund (`ohne Hash`, 2026-10-19) +- `llama/src/main/java/net/ladenthin/llama/Diagnose.java` · `1a229ec` · 2026-10-17 · kein Fund — java-llama.cpp `Diagnose`, `loader/DiagnosticReport`: Backend-Filter-Auswertung (`namedBy`) deckt sich mit `LlamaLoader.selectModules` (inkl. Leer-/Whitespace-Fall); Versionsvergleich per `startsWith` würde nur bei einer 6-stelligen Build-Nummer mit gleichem Präfix irren. +- `llama/src/main/java/net/ladenthin/llama/GgufInspector.java` · `3b4fafc` · 2026-10-18 · Fund F72 — Durchsicht „`GgufInspector.java` ↔ `ggml/src/gguf.cpp` (Schnittstelle GGUF-Metadatenleser ↔ upstream-Leser, b11639)“: Typ-Ids, Kopf (Magie, Version mit Byte-Order-Erkennung, Tensor- und KV-Anzahl) und Grenzen vor Allokation wie zuvor geprüft; Abweichung: Arrays von Arrays, die upstream ablehnt, wurden rekursiv gelesen (F72). ‖ Frühere Durchsichten anderer Blobs: kein Fund (`eda2bb6`, 2026-10-17) +- `llama/src/main/java/net/ladenthin/llama/LlamaIterable.java` · `c7f2128` · 2026-10-17 · kein Fund — `close()` delegiert an `LlamaIterator.close()` (bricht nur ab, solange `hasNext`; mehrfach sicher, F36). `iterator()` liefert stets denselben Iterator, also ist das Iterable einmalig; die Javadoc verspricht nichts anderes. +- `llama/src/main/java/net/ladenthin/llama/LlamaIterator.java` · `f7963f2` · 2026-10-17 · Fund F36, kein Fund (Designabweichung notiert) — (2026-10-17, Fund F36) F36 hasNext nur true→false; cancel/close/Stopp-Freigabe über die Naht TaskSource geprüft ‖ (2026-10-17, kein Fund (Designabweichung notiert)) Durchsicht „`server/OpenAiCompatServer.java` + `server/LlamaModelBackend.java` + `LlamaIterator.java` ↔ `jllama_api.h` (Schnittstelle Streaming-Abbruch und Fehler)“: Client-Abbruch: `IOException` der Senke wird über `UncheckedIOException` aus `streamChatCompletion` getragen, dessen `finally` `cancelCompletion` = `jllama_task_release` ruft (laut Header „cancels or releases“, unbekannte Id ignoriert); nach `stop` gibt der Empfang die Aufgabe selbst frei, `LlamaIterator.next` ruft dann `releaseTask`, `close()` danach ist ein No-op. `streamCompletions` bricht per `break` ab, `LlamaIterable.close()` gibt frei. Fehler nach den Headern als SSE-Ereignis (siehe offene Verdachte). +- `llama/src/main/java/net/ladenthin/llama/LlamaQuantizer.java` · `8fca432` · 2026-10-17 · kein Fund — Null-Prüfung aller drei Referenzen vor dem nativen Aufruf; die Kurzform reicht `threads=0` (`<= 0` = alle Hardware-Threads laut Javadoc und `llama_model_quantize_params.nthread`) und `allowRequantize=false` durch. `LlamaLoader.initialize()` im statischen Block wie bei den übrigen JNI-Klassen. +- `llama/src/main/java/net/ladenthin/llama/RpcServerNative.java` · `ef8f53a` · 2026-10-17 · kein Fund — Reine JNI-Delegation hinter der `RpcServer.Backend`-Naht; Signaturen der vier `native`-Methoden und `@Nullable` (Cache-Verzeichnis, Geräteliste bei unbekanntem Namen) passen zum Interface. `LlamaLoader.initialize()` im statischen Block: das in CLAUDE.md beschriebene Wiedereintreten beim Laden vor `LlamaModel` ist in `LlamaLoader.runOnceOnThisThread` behoben. +- `llama/src/main/java/net/ladenthin/llama/Session.java` · `61d2505` · 2026-10-18 · Fund F137 — Durchsicht „`Session.java` (zweite Durchsicht: `fork`, `checkpoint`/`rewind`, `close`, `buildParams`)“: negative Ziel-ID in `fork` erst nach den Modellaufrufen abgewiesen. `close()` löscht den Slot ohne Stream-Prüfung (dokumentiert: nur KV-Cache, nicht das Transkript); `slotId` wird nach dem Customizer erzwungen. ‖ Frühere Durchsichten anderer Blobs: kein Fund (`7979c5c`, 2026-10-17) +- `llama/src/main/java/net/ladenthin/llama/SessionState.java` · `86ffcc3` · 2026-10-17 · kein Fund — java-llama.cpp `Session` (+ `SessionState`): Zweiphasen-Commit unter einem Lock, ein fehlschlagender Modellaufruf hinterlässt kein halbes Turn-Paar, `slotId` wird nach dem Customizer erzwungen, Fork verlangt einen anderen Slot, alle Slot-Operationen sind während eines Streams gesperrt. +- `llama/src/main/java/net/ladenthin/llama/TextToSpeech.java` · `7b6b88d` · 2026-10-18 · Fund F16, kein Fund — (2026-10-17, Fund F16) F16 Lese-/Schreib-Sperre um synthesize/close, Naht TextToSpeech.Backend; Bibliothek wird beim ersten Laden statt im static-Block geladen ‖ (2026-10-18, kein Fund) Durchsicht „`TextToSpeech.java` ↔ `llama/src/main/cpp/jllama_api.h` (`jllama_tts_*`) / `tts_engine.cpp` (Schnittstelle Lebenszyklus und Nebenläufigkeit)“: Der C-Vertrag erlaubt „one synthesis at a time per handle“, Java nimmt für `synthesize` nur die Lese-Sperre; das ist sicher, weil `tts_engine.cpp` jede Synthese unter dem `synthesize_mutex` der Engine ausführt. `close()` wartet über die Schreib-Sperre auf laufende Synthesen, gibt einmal frei und setzt den Handle auf 0, danach meldet `synthesize` `IllegalStateException`. `seed` (Java `int`) wird zu `unsigned int`; `gpuLayers` 0 im Kurzkonstruktor heißt CPU, −1 (auto) ist explizit übergebbar. +- `llama/src/main/java/net/ladenthin/llama/ToolCallingAgent.java` · `b69653a` · 2026-10-17 · kein Fund — (2026-10-17, kein Fund) java-llama.cpp `ToolCallingAgent`: Rundenzählung = max. Anzahl Chat-Aufrufe, deckt sich mit Javadoc („loops until … the round cap is reached“); Handler-Exceptions/unbekannte Tools werden als `{"error":…}` zurückgegeben. ‖ (2026-10-17, kein Fund) Durchsicht „`ToolCallingAgent.java` + `callback/ToolHandler.java` (Schnittstelle Agent-Schleife ↔ Werkzeug)“: `maxToolRounds` zählt Chat-Aufrufe (mindestens einer, < 1 → IAE wie in `withMaxToolRounds`); Assistenten-Runde und alle Werkzeugergebnisse werden in Reihenfolge mit `tool_call_id` angehängt; unbekanntes Werkzeug und Ausnahme des Handlers werden als `{"error":…}` gemeldet, wie `chatWithTools` dokumentiert. Am Rundenlimit kommt die letzte Antwort zurück, auch mit unerledigten Aufrufen (dokumentiert). Ein Handler, der `null` liefert, verletzt seinen eigenen Vertrag (`@return` ein String) – kein Fund. +- `llama/src/main/java/net/ladenthin/llama/args/CacheType.java` · `e38c636` · 2026-10-17 · kein Fund — Genau die neun Typen von `kv_cache_types` in `common/arg.cpp` (b11639), gleiche Namen. +- `llama/src/main/java/net/ladenthin/llama/args/CliArg.java` · `8516cfd` · 2026-10-17 · kein Fund — Schnittstelle für Enum-Werte auf der Kommandozeile. +- `llama/src/main/java/net/ladenthin/llama/args/CliContract.java` · `a288676` · 2026-10-17 · kein Fund — Zwei Vertragsarten der CLI-Registry. +- `llama/src/main/java/net/ladenthin/llama/args/ContinuationMode.java` · `b01f540` · 2026-10-17 · kein Fund — Zwei Feldnamen der Antwort, als Wert durchgereicht. +- `llama/src/main/java/net/ladenthin/llama/args/DraftSampling.java` · `9c1a37f` · 2026-10-17 · kein Fund — greedy/probabilistic entsprechen der Draft-Sampling-Option in `common/arg.cpp`. +- `llama/src/main/java/net/ladenthin/llama/args/FlashAttn.java` · `310dcc5` · 2026-10-17 · kein Fund — on/off/auto entsprechen `--flash-attn`. +- `llama/src/main/java/net/ladenthin/llama/args/GpuSplitMode.java` · `93b37e4` · 2026-10-17 · kein Fund — none/layer/row/tensor entsprechen `--split-mode`. +- `llama/src/main/java/net/ladenthin/llama/args/LazyMode.java` · `d5225f0` · 2026-10-17 · kein Fund — off/auto/on entsprechen `--lazy-mode`. +- `llama/src/main/java/net/ladenthin/llama/args/LoadMode.java` · `8f8b7be` · 2026-10-17 · kein Fund — Werte auto/none/mmap/mlock/mmap+mlock/dio entsprechen `--load-mode` in `common/arg.cpp`. +- `llama/src/main/java/net/ladenthin/llama/args/LogFormat.java` · `f3f742b` · 2026-10-17 · kein Fund — Zwei Formate. +- `llama/src/main/java/net/ladenthin/llama/args/MiroStat.java` · `691102a` · 2026-10-17 · kein Fund — 0/1/2 entsprechen `--mirostat`. +- `llama/src/main/java/net/ladenthin/llama/args/NumaStrategy.java` · `47fece9` · 2026-10-17 · kein Fund — distribute/isolate/numactl entsprechen `--numa`. +- `llama/src/main/java/net/ladenthin/llama/args/Optimizer.java` · `5d3e2a1` · 2026-10-17 · kein Fund — 0/1 entsprechen `GGML_OPT_OPTIMIZER_TYPE_ADAMW`/`SGD` (b11639, `ggml-opt.h`), die `train_engine.cpp` direkt übergibt. +- `llama/src/main/java/net/ladenthin/llama/args/PoolingType.java` · `376422b` · 2026-10-18 · kein Fund — sechs Werte gegen `include/llama.h` (-1..4) und die CLI-Strings von `--pooling`; `UNSPECIFIED` als Wächter ohne CLI-String, beim einzigen Verbraucher `setPoolingType` fand sich F133. +- `llama/src/main/java/net/ladenthin/llama/args/QuantizationType.java` · `d5ccc9d` · 2026-10-17 · kein Fund — Alle 36 `ftype`-Werte gegen `enum llama_ftype` in `include/llama.h` (b11639) abgeglichen; Namen und Zahlen stimmen. Die upstream entfernten 4/5/6 und 33–35 (`Q4_0_4_4` …) fehlen hier zu Recht, ebenso `GUESSED` (1024, kein Ziel). +- `llama/src/main/java/net/ladenthin/llama/args/ReasoningFormat.java` · `ea87677` · 2026-10-17 · kein Fund — none/auto/deepseek/deepseek-legacy entsprechen `common_reasoning_format_from_name` (b11639, `chat.cpp`). +- `llama/src/main/java/net/ladenthin/llama/args/RopeScalingType.java` · `12ebd19` · 2026-10-17 · kein Fund — `--rope-scaling` nimmt nur none/linear/yarn; `ModelParameters.setRopeScaling` entfernt die Option bei `UNSPECIFIED` und wirft bei `LONGROPE`/`MAX_VALUE`, wie dokumentiert. +- `llama/src/main/java/net/ladenthin/llama/args/Sampler.java` · `3d1661d` · 2026-10-17 · kein Fund — Alle neun Namen in `common_sampler_types_from_names` (b11639, `sampling.cpp`); upstream kennt zusätzlich `top_n_sigma` und `adaptive_p`, die hier fehlen – Vollständigkeit, kein Fund. `setSamplers` verbindet mit `;` wie dokumentiert. +- `llama/src/main/java/net/ladenthin/llama/callback/CancellationToken.java` · `70365a6` · 2026-10-17 · kein Fund (Verhalten) — `volatile`-Flag, `cancel()` idempotent. `LlamaModel.complete(params, token)` setzt den Token bewusst erst im `finally` zurück, nicht beim Eintritt, sodass ein vorab gesetztes `cancel()` wirkt. Doku-Abweichung: Javadoc von `reset()` sagt „Package-private.“, die Methode ist `public` – nur beschrieben (kein Test, Regel Schritt 4). +- `llama/src/main/java/net/ladenthin/llama/callback/LoadProgressCallback.java` · `8eae161` · 2026-10-17 · kein Fund — `false` bricht das Laden ab (siehe Patch 0002 / C-API). +- `llama/src/main/java/net/ladenthin/llama/callback/ToolHandler.java` · `9546b9a` · 2026-10-17 · kein Fund — (2026-10-17, kein Fund) Funktionale Schnittstelle für Werkzeugaufrufe. ‖ (2026-10-17, kein Fund) Durchsicht „`ToolCallingAgent.java` + `callback/ToolHandler.java` (Schnittstelle Agent-Schleife ↔ Werkzeug)“: `maxToolRounds` zählt Chat-Aufrufe (mindestens einer, < 1 → IAE wie in `withMaxToolRounds`); Assistenten-Runde und alle Werkzeugergebnisse werden in Reihenfolge mit `tool_call_id` angehängt; unbekanntes Werkzeug und Ausnahme des Handlers werden als `{"error":…}` gemeldet, wie `chatWithTools` dokumentiert. Am Rundenlimit kommt die letzte Antwort zurück, auch mit unerledigten Aufrufen (dokumentiert). Ein Handler, der `null` liefert, verletzt seinen eigenen Vertrag (`@return` ein String) – kein Fund. +- `llama/src/main/java/net/ladenthin/llama/exception/InvalidRequestException.java` · `971de14` · 2026-10-17 · kein Fund — Unterklasse von `LlamaException` für upstream-400; Zuordnung in `jllama.cpp`/`JllamaNative.check`. +- `llama/src/main/java/net/ladenthin/llama/exception/LlamaException.java` · `5af534c` · 2026-10-17 · kein Fund — Basisklasse, nur Konstruktoren. +- `llama/src/main/java/net/ladenthin/llama/exception/ModelUnavailableException.java` · `9eebb86` · 2026-10-17 · kein Fund — Unterklasse von `LlamaException`, nur Konstruktoren; F56 sorgt dafür, dass `OfflineModelGuard` sie auch bei nicht darstellbaren Pfaden wirft. +- `llama/src/main/java/net/ladenthin/llama/json/ChatResponseParser.java` · `31d8fb7` · 2026-10-18 · kein Fund — (2026-10-17, kein Fund) java-llama.cpp `value/ChatChoice`, `json/ChatResponseParser`: Null-/fehlende Felder über `path().asText(default)` abgefangen; `reasoning_content` landet nicht in `ChatChoice`, ist aber über `getRawJson()`/`extractChoiceReasoningContent` erreichbar (Design, kein Fehler). ‖ (2026-10-17, kein Fund) Durchsicht „`json/ChatResponseParser.java` (Schnittstelle Antwort-JSON ↔ `ChatResponse`/`Usage`)“: `usage.prompt_tokens` (upstream: alle Prompt-Tokens inkl. Cache) und `prompt_tokens_details.cached_tokens` füllen `Usage` so, wie sein Vertrag es verlangt – hier stimmt die Zählung, anders als in `ServerMetrics.getCumulativeUsage` (F57). `content: null` einer Werkzeug-Runde ergibt `""` (`NullNode.asText(default)`), Argumente als String oder Objekt; unparsbare Antwort → leere `ChatResponse` mit Rohtext. ‖ (2026-10-18, kein Fund) Durchsicht „`json/ChatResponseParser.java` (Probe mit 6 Formen)“: `content: null` mit `tool_calls` → leerer Text und ein Aufruf, `finish_reason` übernommen; leere/fehlende `choices`, `{}` und `""` ergeben eine leere Antwort ohne Ausnahme (F37 deckt `""`); fehlende `usage`/`timings` als Nullwerte; eine in ein Array gehüllte Antwort ergibt leer, die native Seite liefert für eine Chat-Antwort aber ein Objekt +- `llama/src/main/java/net/ladenthin/llama/json/CompletionResponseParser.java` · `707296e` · 2026-10-18 · kein Fund, kein Fund (Randnotiz) — (2026-10-17, kein Fund) java-llama.cpp `value/CompletionResult`, `json/CompletionResponseParser`. **Doku-Hinweis (niedrig, nicht geändert):** `TokenLogprob.getLogprob()`-Javadoc sagt „a value in [0, 1] is a probability; a non-positive value is a natural-log probability“. Der Wert **0.0** fällt in beide Fälle: `logprob =… ‖ (2026-10-18, kein Fund (Randnotiz)) Durchsicht „`value/StopReason.java` + `json/CompletionResponseParser.java` + `LlamaModelBackend.completionFinishReason` ↔ upstream `stop_type_to_str` / `finish_reason` (`server-task.cpp:243`, `:375`, b11639)“: Die vier Werte `eos`/`word`/`limit`/`none` entsprechen genau upstreams `stop_type`; `stop_type` wird nur bei `stop: true` ausgewertet. Upstream setzt `finish_reason` standardmäßig auf `"length"` und nur bei `word`/`eos` auf `"stop"`; `completionFinishReason` mappt dagegen nur `MAX_TOKENS` auf `"length"` und alles andere auf `"stop"` – unterschiedlich nur für ein Endergebnis mit `stop_type` `none`, das im normalen Ablauf kaum vorkommt. ‖ (2026-10-18, kein Fund) Durchsicht „`json/CompletionResponseParser.java` + `value/StopReason.java` ↔ upstream `stop_type_to_str` (Schnittstelle Stopptypen, b11639)“: upstream kennt `eos`/`word`/`limit` (dazu `none` für Zwischenschritte), `StopReason` bildet alle vier ab; der gestreamte Completion-Weg macht `limit` zu `length`, sonst `stop`, wie upstreams `finish_reason`. ‖ (2026-10-18, kein Fund) Durchsicht „`json/CompletionResponseParser.java` (Probe mit 7 Formen)“: `stop_type` `limit`/`word`/`eos`/`none` → `MAX_TOKENS`/`STOP_STRING`/`EOS`/`NONE`; Wahrscheinlichkeiten vor (`logprob`, `top_logprobs`, `id`) und nach dem Sampling (`prob`, `top_probs`, ohne `id` → −1); dass ein `prob` in `TokenLogprob.getLogprob()` landet, ist dokumentiert (Wert unverändert gespeichert, [0,1] = Wahrscheinlichkeit, ≤ 0 = Log) +- `llama/src/main/java/net/ladenthin/llama/json/EmbeddingResponseParser.java` · `6f2a458` · 2026-10-18 · kein Fund — (2026-10-17, kein Fund) java-llama.cpp `json/EmbeddingResponseParser`: übersprungene Nicht-Array-Einträge würden die Zuordnung verschieben, `LlamaModel.embed(Collection)` prüft aber die Anzahl (`LlamaModel.java:431`) und wirft; nach `index` stabil sortiert. ‖ (2026-10-18, kein Fund) Durchsicht „`json/EmbeddingResponseParser.java` (Probe mit 9 Formen)“: liest nur die OAI-Form `{data:[{index,embedding}]}` und sortiert nach `index`; native Formen ergeben leer (werden über diesen Weg nie geliefert); ein Base64-Eintrag würde übersprungen und die Zuordnung verschieben (schon am 17. notiert), `toBatchRequestJson` fordert aber nie `encoding_format` an; ein verschachtelter Vektor ergäbe Unsinn, upstream lehnt `pooling none` am OAI-Endpunkt aber ab +- `llama/src/main/java/net/ladenthin/llama/json/LoraAdapterResponseParser.java` · `6a637ed` · 2026-10-18 · kein Fund — (2026-10-17, kein Fund) `readTree("")` liefert `MissingNode`, dessen `isArray()` falsch ist → leere Liste wie dokumentiert (F37-Muster hier unschädlich). `toRequestJson` weist NaN/±∞ ab (würde nicht als JSON-Zahl serialisieren); unbekannte Ids ignoriert die native Seite tatsächlich (`parse_lora_request` füllt eine `std::map`, angewandt wird nur über geladene Adapter), passend zur Javadoc von `LlamaModel.setLoraAdapters`. `null`-Schlüssel/-Werte → NPE, für `Map` Aufruferfehler. ‖ (2026-10-18, kein Fund) Durchsicht „`json/LoraAdapterResponseParser.java` (↔ upstream `server_task_result_get_lora::to_json`, `parse_lora_request`, b11639)“: liest `id`/`path`/`scale`/`task_name`/`prompt_prefix` wie upstream sie schreibt (aLoRA-Felder bleiben ungenutzt), Nicht-Array → leer; `toRequestJson` schreibt `{id, scale}` wie `parse_lora_request` liest und lehnt `NaN`/unendlich vorher ab +- `llama/src/main/java/net/ladenthin/llama/json/TimingsLogger.java` · `8327d64` · 2026-10-19 · kein Fund — (2026-10-17, kein Fund) Zahlen mit `Locale.ROOT` formatiert (kein Dezimalkomma); Leerlauf-Prüfung `promptN == 0 && predictedN == 0` passt zu `Timings.fromJson`, das bei fehlendem/`null`-Knoten und fehlenden Feldern 0 liefert (upstream lässt `timings` bei `prompt_n < 0` ganz weg). Draft-Segment nur bei `draftN > 0`; Aufrufer `CompletionResponseParser`/`ChatResponseParser` übergeben nie `null`. `isInfoEnabled` vor der String-Bildung. ‖ (2026-10-19, kein Fund) Durchsicht „`json/TimingsLogger.java` (zweite Durchsicht)“: `Locale.ROOT`-Formatierung, leere Timings (0/0) und `null` werden übersprungen, Draft-Teil nur bei spekulativer Dekodierung. +- `llama/src/main/java/net/ladenthin/llama/loader/DiagnosticReport.java` · `a15ed7b` · 2026-10-19 · kein Fund — (2026-10-17, kein Fund) java-llama.cpp `Diagnose`, `loader/DiagnosticReport`: Backend-Filter-Auswertung (`namedBy`) deckt sich mit `LlamaLoader.selectModules` (inkl. Leer-/Whitespace-Fall); Versionsvergleich per `startsWith` würde nur bei einer 6-stelligen Build-Nummer mit gleichem Präfix irren. ‖ (2026-10-19, kein Fund) Durchsicht „`loader/DiagnosticReport.java` ↔ `LlamaLoader.selectModules`“: `namedBy` teilt und trimmt den Filter wie der Loader (Groß-/Kleinschreibung in beiden exakt), der Hinweis auf ungenutzte Opt-in-Module stimmt auch bei leerem Filter (der Loader behandelt ihn als unset). Commit-Kürzung nur für volle 40-Zeichen-Hashes, `-dirty` bleibt erhalten; jeder Prüfschritt fängt RuntimeException/LinkageError ab, ein Bericht scheitert also nie an einer fehlenden nativen Bibliothek. +- `llama/src/main/java/net/ladenthin/llama/loader/LlamaSystemProperties.java` · `ce16589` · 2026-10-17 · kein Fund — Reine Zugriffsschicht auf `System.getProperty`, `@Nullable` korrekt. Die fünf Namen (`lib.path`, `tmpdir`, `osinfo.architecture`, `test.ngl`, `backend`) stimmen mit der Systemeigenschaften-Tabelle der README überein. +- `llama/src/main/java/net/ladenthin/llama/loader/NativeLibraryPermissionSetter.java` · `e2c6e1b` · 2026-10-17 · kein Fund — Setzt Lesen/Schreiben/Ausführen jeweils nur für den Besitzer (`setReadable(true)`/`setExecutable(true)` sind die Besitzer-Varianten, `setWritable(true, true)` explizit) – für eine extrahierte native Bibliothek die vorsichtigere Wahl. Ein Fehlschlag wird mit allen drei Ergebnissen gemeldet und als `false` zurückgegeben, nicht verschluckt; die Senke ist im Konstruktor auf `null` geprüft. +- `llama/src/main/java/net/ladenthin/llama/loader/OSInfo.java` · `138b590` · 2026-10-18 · kein Fund — (2026-10-17, kein Fund) java-llama.cpp `loader/OSInfo`: Abbildung von `os.name`/`os.arch` auf die Natives-Verzeichnisse für alle ausgelieferten Plattformen (Linux/Windows x86_64 + aarch64, Mac aarch64, s390x, Android) korrekt. Randnotizen (niedrig): `isMusl()` fällt nur bei einer Ausnahme von `Files.list` auf `/etc/os-r… ‖ (2026-10-18, kein Fund) Durchsicht „`loader/OSInfo.java` (zweite Durchsicht: `getArchName`/`resolveArmArchType`/`translateOSNameToFolderName` gegen die Verzeichnisse aus `natives.csv`)“: `aarch64` steht nicht in `archMapping`, fällt aber unverändert durch `translateArchNameToFolderName` (Linux, Windows, Mac und Android aarch64 → `aarch64`), `amd64`/`x86_64` → `x86_64`, `s390x` gemappt; Android vor Musl vor Linux geprüft. Nicht verfolgt: eine JVM, die `os.arch`=`arm64` meldet, landete auf macOS über `resolveArmArchType` bei `arm` (OpenJDK-Builds melden `aarch64`); `isMusl` sucht "musl" in jedem gemappten Pfad, ein Verzeichnisname mit "musl" auf glibc würde falsch erkannt. +- `llama/src/main/java/net/ladenthin/llama/loader/ProcessRunner.java` · `372d6f4` · 2026-10-17 · kein Fund — Liest stdout erst nach `waitFor`, stderr gar nicht: ein Kind mit mehr als einem Pipe-Puffer (~64 KiB) Ausgabe bliebe bis zum Timeout hängen. Nicht erreichbar: einzige Aufrufer sind `OSInfo.isAndroidTermux` (`uname -o`) und `getHardwareName` (`uname -m`), je eine Zeile; der Pfad mit viel Ausgabe (`readelf`) läuft über `runDiscardingOutput`, das vor `waitFor` leert. Timeout → IOException, `destroyForcibly` im finally, Abschluss-Newline bei `startsWith`/`contains` harmlos. Musterdurchsicht nach F41 (abgeschnittene Exception-Meldungen) über alle Ziele: kein weiterer Treffer. +- `llama/src/main/java/net/ladenthin/llama/parameters/RequestContract.java` · `b216881` · 2026-10-17 · kein Fund — Zwei Vertragsarten der Request-Registry. +- `llama/src/main/java/net/ladenthin/llama/parameters/TrainingField.java` · `8a305f9` · 2026-10-18 · kein Fund — 15 Schlüssel gegen `TrainingParameters.toJson` (jeder genau einmal geschrieben) und gegen `jllama_train::config_keys()` (durch `test_wire_contracts.cpp` gehalten); `validate()` prüft Korpus, Epochen und `valSplit`. +- `llama/src/main/java/net/ladenthin/llama/server/ChunkSink.java` · `02cb145` · 2026-10-17 · kein Fund — Funktionale Schnittstelle, `IOException` signalisiert einen getrennten Client. +- `llama/src/main/java/net/ladenthin/llama/server/ServerLauncher.java` · `d8433a0` · 2026-10-19 · kein Fund — (2026-10-17, kein Fund) java-llama.cpp `server/ServerLauncher`: entfernt jedes Vorkommen des Markers und dispatcht korrekt; trivial. ‖ (2026-10-19, kein Fund) Durchsicht „`server/ServerLauncher.java` (zweite Durchsicht)“: der Marker wird an jeder Stelle und mehrfach entfernt, sonst gehen die Argumente unverändert an `NativeServer.main`. +- `llama/src/main/java/net/ladenthin/llama/server/ToolCallDeltaAccumulator.java` · `58a9bab` · 2026-10-18 · kein Fund — (2026-10-17, kein Fund) Gruppierung nach `index` (TreeMap, sortierte Ausgabe), Argument-Deltas werden verkettet, `id`/`name` aus dem ersten Delta; ein Aufruf ohne Argument-Deltas ergibt `arguments: ""`, was erst der Ollama-Konsument falsch abbildete (F37). ‖ (2026-10-17, kein Fund) Durchsicht „`server/ToolCallDeltaAccumulator.java` (Schnittstelle gegen upstreams Tool-Call-Deltas, b11639)“: Upstream (`common/chat.cpp` Diff, `server-chat.cpp` Ausgabe) sendet bei geändertem Namen oder geänderter ID den **vollständigen** neuen Wert (ein wachsender Name kommt also ganz, nicht als Anhang), die Argumente dagegen als Differenz, und leere Felder gar nicht. Genau so verarbeitet der Akkumulator: `id`/`name` überschreiben, `arguments` anhängen, je `index` getrennt, in Indexreihenfolge ausgegeben (`TreeMap`). ‖ (2026-10-18, kein Fund) nach `index` geordnet (`TreeMap`), `id`/`name` ersetzt, `arguments` angehängt; passt zu upstreams Diff-Form (Name und Id einmal, danach nur Argumentstücke); ein Chunk ohne `index` zählt als 0 ‖ (2026-10-18, kein Fund) Durchsicht „`server/ToolCallDeltaAccumulator.java` ↔ `AnthropicStreamTranslator` / `ResponsesStreamTranslator` / `OllamaApiSupport` (Musterdurchsicht nach F128: Fragmente ohne Argumente oder vor dem Namen)“: der Akkumulator übernimmt `id`/`name` und hängt Argumente an, ohne etwas weiterzumelden; alle drei Übersetzer geben `tool_use`- bzw. `function_call`-Blöcke und Ollama-`tool_calls` erst am Ende aus dem fertigen Stand aus (`asText("")` für einen nie gelieferten Namen), es gibt keinen Teilaufruf-Event mit Vorbedingungen. Die frühere Prüfzeile zu `StreamingChunkAssembler` (17.10., „gleiche Semantik“) hatte die langchain4j-Vorbedingungen nicht betrachtet – siehe F128. +- `llama/src/main/java/net/ladenthin/llama/value/ChatChoice.java` · `ad1c75f` · 2026-10-17 · kein Fund — java-llama.cpp `value/ChatChoice`, `json/ChatResponseParser`: Null-/fehlende Felder über `path().asText(default)` abgefangen; `reasoning_content` landet nicht in `ChatChoice`, ist aber über `getRawJson()`/`extractChoiceReasoningContent` erreichbar (Design, kein Fehler). +- `llama/src/main/java/net/ladenthin/llama/value/ChatMessage.java` · `453a951` · 2026-10-18 · kein Fund — (2026-10-17, kein Fund) `parts` null/leer → IAE wie dokumentiert, Kopie unveränderlich; `toolCalls` null → leere Liste. Javadoc-Versprechen „Parts haben beim Serialisieren Vorrang“ in der einzigen Serialisierung (`ParameterJsonSerializer.buildMessages(List)`, genutzt von `ChatRequest.buildMessagesJson` und `InferenceParameters.withMessages`) erfüllt, `tool_call_id`/`tool_calls` werden mitgeschrieben. `Session`/`ChatTranscript` bauen nur Textnachrichten. Ein `null`-Element in `parts` ergäbe eine NPE statt IAE – Javadoc schweigt dazu, kein Fund. ‖ (2026-10-18, kein Fund) Durchsicht „`value/ChatMessage.java` (zweite Durchsicht)“: Teile und Tool-Calls defensiv kopiert und unveränderlich zurückgegeben, `content` aus den Textteilen mit Zeilenumbruch verbunden, leere Teil-Liste abgewiesen. Ein `null`-Element in der Teil-Liste ergäbe eine NullPointerException beim Zusammenfügen (Aufruferfehler, nicht verfolgt). +- `llama/src/main/java/net/ladenthin/llama/value/ChatResponse.java` · `7b68d5f` · 2026-10-17 · kein Fund — `choices` null → leere Liste; `getFirstMessage`/`getFirstContent` behandeln leere Antworten. +- `llama/src/main/java/net/ladenthin/llama/value/ChatTranscript.java` · `cf96a79` · 2026-10-18 · kein Fund — (2026-10-17, kein Fund) Zweiphasen-Commit wie dokumentiert: `messagesWithPendingUserTurn` kopiert, mutiert nicht; `removePendingUserTurn` entfernt nur eine abschließende `user`-Runde. Leere/`null`-Systemnachricht wird in `snapshot()` und in `ParameterJsonSerializer.buildMessages(String, List)` gleich behandelt (weggelassen). Nicht synchronisiert, wie die Javadoc sagt; `Session` hält das Lock (bereits geprüft). `turnsSnapshot`/`resetTurns` kopieren, `Pair` ist unveränderlich. ‖ (2026-10-18, kein Fund) Durchsicht „`value/ChatTranscript.java` (zweite Durchsicht)“: `removePendingUserTurn` entfernt nur eine offene Nutzerzeile am Ende, `messagesWithPendingUserTurn` kopiert, `snapshot` lässt eine leere Systemnachricht weg, `turnsSnapshot`/`resetTurns` arbeiten auf Kopien (Pair ist unveränderlich). +- `llama/src/main/java/net/ladenthin/llama/value/CompletionResult.java` · `934a56d` · 2026-10-17 · kein Fund — java-llama.cpp `value/CompletionResult`, `json/CompletionResponseParser`. **Doku-Hinweis (niedrig, nicht geändert):** `TokenLogprob.getLogprob()`-Javadoc sagt „a value in [0, 1] is a probability; a non-positive value is a natural-log probability“. Der Wert **0.0** fällt in beide Fälle: `logprob =… +- `llama/src/main/java/net/ladenthin/llama/value/ContentPart.java` · `e167fa7` · 2026-10-18 · kein Fund — (2026-10-17, kein Fund) java-llama.cpp `value/ContentPart`: Validierung ok. Randnotiz: Lombok-`@ToString` gibt die komplette Base64-Nutzlast (`imageUrl`/`audioData`, schnell mehrere MB) aus – teuer, falls ein `ChatMessage`/`ChatRequest` geloggt wird. ‖ (2026-10-18, kein Fund) Durchsicht „`value/ContentPart.java` (zweite Durchsicht)“: MIME/Format aus der Endung ohne Locale-Falle (`Locale.ROOT`), Data-URL `data:;base64,`, Audioformat auf `wav`/`mp3` normalisiert; `@EqualsAndHashCode` (Lombok) trägt die Wertgleichheit, die `ChatRequest` verspricht. +- `llama/src/main/java/net/ladenthin/llama/value/GgufMetadata.java` · `6261a64` · 2026-10-17 · kein Fund — java-llama.cpp `GgufInspector`, `value/GgufMetadata`: Endianness-Erkennung über das Versionsfeld nachgerechnet (BE v3 → `0x03000000` → reverse → 3), Längen-/Anzahl-Grenzen vor Allokation geprüft. Nur Randnotiz: Summe aller Strings eines Arrays ist nicht begrenzt (2^26 × bis 2^27 Byte) – bei einer… +- `llama/src/main/java/net/ladenthin/llama/value/LlamaCppVersion.java` · `519fc3a` · 2026-10-17 · kein Fund — `b11639` stimmt mit `JLLAMA_GIT_TAG` in `llama/CMakeLists.txt` überein. +- `llama/src/main/java/net/ladenthin/llama/value/LlamaOutput.java` · `e928537` · 2026-10-17 · kein Fund — reiner Träger, `toString` gibt den Text wie dokumentiert. +- `llama/src/main/java/net/ladenthin/llama/value/LogLevel.java` · `6662b54` · 2026-10-17 · kein Fund — Vier Stufen, Abbildung der ggml-Stufen liegt im JNI-Code (außerhalb des Ziels). +- `llama/src/main/java/net/ladenthin/llama/value/LoraAdapter.java` · `0351765` · 2026-10-17 · kein Fund — java-llama.cpp `value/LoraAdapter` (+ `json/LoraAdapterResponseParser`): Schlüssel `id/path/scale/task_name/prompt_prefix` gegen `server-task.cpp` b11639 identisch; das Javadoc „nicht gelistete Adapter werden auf 0 gesetzt“ stimmt mit `construct_lora_list` überein. +- `llama/src/main/java/net/ladenthin/llama/value/ModelMeta.java` · `e9aa330` · 2026-10-18 · kein Fund — (2026-10-17, kein Fund) java-llama.cpp `value/ModelMeta`: alle Getter-Schlüssel gegen den Erzeuger in `jllama_engine.cpp` abgeglichen (`vocab_type`, `n_ctx_train`, `n_params`, `size`, `modalities.*`, `input/output_modalities`, `special_tokens.bos/eos/eot`, `metadata`, `chat_template`, …) – identisch; Default `-1` für fe… ‖ (2026-10-18, kein Fund) Durchsicht „`value/ModelMeta.java` ↔ `jllama_engine.cpp` (Modell-Meta-JSON)“: alle 21 gelesenen Schlüssel/Pfade werden nativ geschrieben (`modalities.{vision,audio,video}`, `special_tokens.{bos,eos,eot}`, `metadata`, Ein-/Ausgabemodalitäten), Vorgaben für fehlende Felder passen zum Vocab-only-Zweig (der nur `vocab_type`, `n_vocab`, `special_tokens` liefert). `vocab_type` wird nativ als int gecastet (common_json-Falle, CLAUDE.md). Außerhalb des Bereichs bemerkt (C++, nicht geändert): Metadaten-Schlüssel landen in einem festen 256-Byte-Puffer, längere würden abgeschnitten. +- `llama/src/main/java/net/ladenthin/llama/value/Pair.java` · `a209738` · 2026-10-17 · kein Fund — Unveränderlicher Träger, `equals`/`hashCode` über beide Felder. +- `llama/src/main/java/net/ladenthin/llama/value/RpcEndpoint.java` · `a6b12c5` · 2026-10-18 · kein Fund — (2026-10-17, kein Fund) java-llama.cpp `value/RpcEndpoint`: Parser (genau ein ':', Port 1..65535, max. 5 Ziffern vor `parseInt` → kein Überlauf), Host-Zeichensatz und Länge, `parseList` mit leeren Teilen korrekt; equals/hashCode über host+port. ‖ (2026-10-18, kein Fund) Durchsicht „`value/RpcEndpoint.java` (Probe mit 21 Schreibweisen)“: fehlender/leerer/0/65536/negativer/`+80`-Port, leerer Host, IPv6 mit und ohne Klammern und `host:80:90` werden mit klarer Meldung abgelehnt; `080` wird 80 (wie upstreams `std::stoi`), Leerraum um das Ganze getrimmt, nicht zwischen Doppelpunkt und Port; `parseList` überspringt leere Einträge und lehnt eine leere Liste ab. `GgufInspector` erneut gegen feindliche Längen gehalten: Obergrenzen für Schlüsselzahl, Stringlänge und Arraylänge, Strings werden nur aus vorhandenen Bytes gelesen (F72 deckt die Verschachtelung) +- `llama/src/main/java/net/ladenthin/llama/value/SessionCheckpoint.java` · `15a5811` · 2026-10-17 · kein Fund — Runden werden kopiert und unveränderlich gespeichert, `Pair` ist unveränderlich; `toString` nennt Pfad und Rundenzahl. +- `llama/src/main/java/net/ladenthin/llama/value/SlotMetrics.java` · `0ddd441` · 2026-10-17 · kein Fund — java-llama.cpp `value/SlotMetrics` (+ `ServerMetrics.getSlotMetrics`): alle gelesenen Schlüssel (`id`, `n_ctx`, `is_processing`, `n_prompt_tokens{,_processed,_cache}`, `next_token[0].n_remain/n_decoded`) gegen `server_slot::to_json` bei b11639 abgeglichen – identisch, `next_token` ist dort ein Ar… +- `llama/src/main/java/net/ladenthin/llama/value/StopReason.java` · `cb51433` · 2026-10-18 · kein Fund, kein Fund (Randnotiz) — (2026-10-17, kein Fund) Namen gegen `stop_type_to_str` (b11639, `server-task.cpp`) abgeglichen: `eos`/`word`/`limit`, `none` und Unbekanntes → `NONE`. ‖ (2026-10-18, kein Fund (Randnotiz)) Durchsicht „`value/StopReason.java` + `json/CompletionResponseParser.java` + `LlamaModelBackend.completionFinishReason` ↔ upstream `stop_type_to_str` / `finish_reason` (`server-task.cpp:243`, `:375`, b11639)“: Die vier Werte `eos`/`word`/`limit`/`none` entsprechen genau upstreams `stop_type`; `stop_type` wird nur bei `stop: true` ausgewertet. Upstream setzt `finish_reason` standardmäßig auf `"length"` und nur bei `word`/`eos` auf `"stop"`; `completionFinishReason` mappt dagegen nur `MAX_TOKENS` auf `"length"` und alles andere auf `"stop"` – unterschiedlich nur für ein Endergebnis mit `stop_type` `none`, das im normalen Ablauf kaum vorkommt. ‖ Frühere Durchsichten anderer Blobs: kein Fund (`ohne Hash`, 2026-10-18) +- `llama/src/main/java/net/ladenthin/llama/value/Timings.java` · `e187e37` · 2026-10-17 · kein Fund — Schlüssel von `fromJson` gegen upstreams `server_slot_stats::to_json()` (b11639, `server-common.cpp`) abgeglichen: `cache_n`, `prompt_n/_ms/_per_second`, `predicted_n/_ms/_per_second` stimmen; `draft_n`/`draft_n_accepted` sendet upstream nur bei Draft-Tokens > 0, der Standardwert 0 passt. `null`/Missing/JSON-null → Nullobjekt. `*_per_token_ms` wird nicht gelesen (kein Getter versprochen). +- `llama/src/main/java/net/ladenthin/llama/value/TokenLogprob.java` · `645a0f9` · 2026-10-17 · kein Fund — java-llama.cpp `value/CompletionResult`, `json/CompletionResponseParser`. **Doku-Hinweis (niedrig, nicht geändert):** `TokenLogprob.getLogprob()`-Javadoc sagt „a value in [0, 1] is a probability; a non-positive value is a natural-log probability“. Der Wert **0.0** fällt in beide Fälle: `logprob =… +- `llama/src/main/java/net/ladenthin/llama/value/ToolDefinition.java` · `1b8732b` · 2026-10-17 · kein Fund — unveränderlicher Träger; das Schema bleibt Text, die Prüfung leerer/ungültiger Schemata liegt in `ChatRequest.buildToolsJson` (F55). +- `llama/src/main/java/net/ladenthin/llama/value/Usage.java` · `ce0b9a7` · 2026-10-17 · Fund F57 (beim Aufrufer `ServerMetrics`) — Klasse selbst korrekt: `getProcessedPromptTokens` auf ≥ 0 begrenzt, `getTotalTokens` = prompt + completion wie bei OAI. + +## Abgelaufen – erneut zu prüfen + +Diese Dateien wurden nach ihrer letzten Lesung geändert; die Zeile nennt den damals gelesenen Blob und was sich +seitdem geändert hat. Sie gelten als ungeprüft, bis jemand sie erneut liest. + +- `llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmLlamaModel.java` · `41c9e8b` · 2026-10-18 · kein Fund (nur gelesen, kein JDK 22 hier) — Durchsicht „`FfmTaskStream.java` + `FfmLlamaModel.java` (`receive`, `releaseTask`, `close`) ↔ `jllama_api.h` (Schnittstelle Streaming-Aufgabe über FFM)“: Nach `stop` gibt `next()` die Aufgabe genau einmal frei (`released`), ein frühes `close()` ebenso; wirft der Empfang (Fehlerergebnis), hat die Engine den Reader schon entfernt, ein späteres `close()` trifft eine unbekannte Id, die laut Header ignoriert wird. Nach `model.close()` ruft `releaseTask` nichts mehr auf. Das Prüfen-dann-Aufrufen von `handle()` gegen ein paralleles `close()` ist in der Klassen-Javadoc als Vertrag festgehalten (siehe F59); deren Vergleich „as with `LlamaModel`“ stimmt nur halb, denn das JNI-`LlamaModel` liest den Handle unter `g_ctx_mutex` und übersteht ein paralleles `close()`. ‖ **Abgelaufen:** Blob am HEAD `5b065ae`, seitdem 2 Commits an der Datei, darunter der Fix F186. Frühere Durchsichten: kein Fund (`41c9e8b`, 2026-10-17). +- `llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmNativeServer.java` · `21145a1` · 2026-10-17 · Fund F59 — Race `isRunning`/`close`. Sonst stimmen die Bindungen mit `jllama_api.h` überein: `start(argc, argv, out)`, `start_attached(model, argc, argv, out)`, `stop`, `is_running` mit `JAVA_BOOLEAN`, `set_worker_command` mit `""` für „löschen“ wie dokumentiert; `argv[0]` = Programmname. ‖ **Abgelaufen:** Blob am HEAD `4351a53`, seitdem 1 Commit an der Datei. Frühere Durchsichten: kein Fund (`ohne Hash`, 2026-10-18). +- `llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmRpcServer.java` · `7b5e31f` · 2026-10-17 · kein Fund — Signaturen passen zu `jllama_rpc_server_serve/stop/listening/devices`; `cacheDir` darf null sein (C-Vertrag), `devices` null → `""`. Das bekannte `takeStrings`-Leck bei Anzahl 0 steht als offener Verdacht im Fundbuch. ‖ **Abgelaufen:** Blob am HEAD `3e17643`, seitdem 1 Commit an der Datei, darunter der Fix F241. +- `llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmTextToSpeech.java` · `8d99f3b` · 2026-10-17 · Fund F16 — java-llama.cpp: `close()` parallel zu `synthesize()` gibt die TTS-Engine frei, während sie läuft (Use-after-free → JVM-Absturz) ‖ **Abgelaufen:** Blob am HEAD `51ac5db`, seitdem 1 Commit an der Datei, darunter der Fix F198. +- `llama-ffm/src/main/java/net/ladenthin/llama/ffm/JllamaNative.java` · `2e2c48f` · 2026-10-17 · Fund F168 — java-llama.cpp `llama-ffm/.../ffm/JllamaNative.java` `takeStrings`: Bei `count == 0` kehrt die Methode vor dem `try` zurück und gibt das Array nicht frei, das `jllama_rpc_server_devices` auch für 0 Einträge per `calloc(max(n,1))` anlegt (8 Byte Leck je Aufruf). `rpc::server_devices()` fällt aber … **Nachtrag (Branch-Review 2026-10-20):** als F168 behoben (`takeStrings` gibt das Array auch bei 0 Einträgen frei). ‖ **Abgelaufen:** Blob am HEAD `a7490f1`, seitdem 2 Commits an der Datei, darunter der Fix F186. +- `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/StreamingChunkAssembler.java` · `b224a28` · 2026-10-18 · kein Fund — Durchsicht „`LangChain4jMapping.java` (`toFinishReason`) + `StreamingChunkAssembler.java` (`finish_reason`/Usage im Stream)“: `stop`/`length`/`tool_calls`/`content_filter` auf die passenden Enum-Werte, fehlend → `STOP`, Unbekanntes → `OTHER`; im Stream wird `finish_reason` nur übernommen, wenn es ein String ist (JSON-`null` der Zwischen-Chunks überschreibt nichts), Usage aus dem Chunk mit leeren `choices` ‖ **Abgelaufen:** Blob am HEAD `42fa7fd`, seitdem 4 Commits an der Datei, darunter der Fix F242. Frühere Durchsichten: kein Fund (`b224a28`, 2026-10-17); Fund F128 (`dd4d519`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/BatchDispatch.java` · `3cd7b3d` · 2026-10-20 · Fund F200 — `runEachOnItsOwnThread` hatte einen Thread je Anfrage ohne Obergrenze und warf einen `Error` vor dem Abwarten der übrigen Anfragen (beides F200, behoben). `submitAllThenReceive`: alle Anfragen werden vor dem ersten Empfang auf dem rufenden Thread abgeschickt, bei einem Fehler beim Abschicken die schon abgeschickten freigegeben, bei einem Fehler beim Empfang die übrigen trotzdem empfangen — kein Fund. ‖ **Abgelaufen:** Blob am HEAD `53527b7`, seitdem 2 Commits an der Datei, darunter die Fixes F235, F236. +- `llama/src/main/java/net/ladenthin/llama/LlamaModel.java` · `96eafab` · 2026-10-18 · kein Fund — Durchsicht „`LlamaModel.java` (`complete(params, token)` ↔ `stream_next`)“: bei nativem Fehler gibt `stream_next` den Reader selbst frei, ein fehlendes `cancelCompletion` im Fehlerpfad leckt also nichts ‖ **Abgelaufen:** Blob am HEAD `ef98aab`, seitdem 9 Commits an der Datei, darunter die Fixes F176, F177, F186, F200, F236. Frühere Durchsichten: Fund F67 (`96eafab`, 2026-10-17); Verdacht notiert (`96eafab`, 2026-10-18); kein Fund (`96eafab`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/LlamaTrainer.java` · `9d751fc` · 2026-10-17 · kein Fund (Verhalten) — Native Rückgabe `null`/leer = Erfolg, sonst `LlamaException` mit der Meldung; Konfiguration wird vor dem nativen Aufruf über `toJson()` geprüft (F01). Javadoc nennt die `IllegalArgumentException` dieser Prüfung nicht – als Doku-Lücke beim F01-Eintrag notiert. ‖ **Abgelaufen:** Blob am HEAD `72247c3`, seitdem 1 Commit an der Datei. Frühere Durchsichten: Fund F71 (`ohne Hash`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/RpcServer.java` · `0585de5` · 2026-10-18 · kein Fund — Durchsicht „`RpcServer.java` (zweite Durchsicht nach F30: `start`, `close`, `Options.parse`, `awaitListening`/`stopAndJoin`; Musterdurchsicht nach F140 – wirft ein früher Schritt nach dem Belegen von `ACTIVE`?)“: `ACTIVE` wird bei jedem gescheiterten Start im `finally` freigegeben, auch wenn die Geräteauflösung wirft; der Server-Thread ist Daemon, ein gescheiterter Start hält die JVM also nicht. Randfall notiert: endet der Server-Thread in `close()` nicht binnen `STOP_TIMEOUT_MILLIS` (30 s), gibt `close()` `ACTIVE` trotzdem frei, und ein neuer Start träfe auf ggmls noch belegten globalen Serverzustand – nicht reproduzierbar ohne native Bibliothek. ‖ **Abgelaufen:** Blob am HEAD `98bb23f`, seitdem 3 Commits an der Datei, darunter die Fixes F237, F241. Frühere Durchsichten: Fund F30 (`0585de5`, 2026-10-17). +- `llama/src/main/java/net/ladenthin/llama/args/ModelFlag.java` · `0807fe4` · 2026-10-17 · übersprungen — java-llama.cpp `args/ModelFlag`: reine Wire-Name-Registry, gegen den echten llama.cpp-Parser durch `test_model_flags.cpp` erzwungen – kein eigenständig prüfbarer Code. ‖ **Abgelaufen:** Blob am HEAD `613ae97`, seitdem 1 Commit an der Datei. +- `llama/src/main/java/net/ladenthin/llama/args/ModelOption.java` · `729cbc6` · 2026-10-17 · kein Fund — 110 Konstanten, 110 verschiedene Namen, keiner doppelt mit `ModelFlag`; alle 110 Namen kommen in `common/arg.cpp` von b11639 vor (die Server-Parser-Zuordnung prüft `test_model_flags.cpp`). Randnotiz: die Javadoc von `ModelFlag.getCliFlag()` nennt `--flash-attn` als Beispiel, das ist seit der Umstellung ein `ModelOption` – Stil, kein Fund. ‖ **Abgelaufen:** Blob am HEAD `d088bb3`, seitdem 1 Commit an der Datei, darunter der Fix F185. +- `llama/src/main/java/net/ladenthin/llama/json/ChatStreamChunkParser.java` · `2448442` · 2026-10-19 · kein Fund — Durchsicht „`json/ChatStreamChunkParser.java` (zweite Durchsicht)“: Array/Objekt/fehlend wie dokumentiert, unlesbarer oder leerer Umschlag beendet den Strom (verhindert eine Endlosschleife). ‖ **Abgelaufen:** Blob am HEAD `2cd32b3`, seitdem 1 Commit an der Datei. Frühere Durchsichten: Fund F53 (`2448442`, 2026-10-17). +- `llama/src/main/java/net/ladenthin/llama/json/RerankResponseParser.java` · `2591bb0` · 2026-10-19 · kein Fund — Durchsicht „`json/RerankResponseParser.java` ↔ `rerank_results_to_json`“: `document` und `score` werden nativ geschrieben. Kleinigkeit: `parse(String)` liefert bei unlesbarem JSON eine unveränderliche, `parse(JsonNode)` bei leerem Array eine veränderliche Liste (der Kommentar dort nennt gerade diese Einheitlichkeit als Grund). ‖ **Abgelaufen:** Blob am HEAD `675daa0`, seitdem 1 Commit an der Datei, darunter der Fix F176. Frühere Durchsichten: kein Fund (`2591bb0`, 2026-10-17). +- `llama/src/main/java/net/ladenthin/llama/json/RouterModelsResponseParser.java` · `fd64a69` · 2026-10-18 · kein Fund — `status.value`/`failed`/`exit_code`, `architecture.*_modalities`, `context_length` wie upstream emittiert ‖ **Abgelaufen:** Blob am HEAD `cb49f91`, seitdem 1 Commit an der Datei, darunter der Fix F175. Frühere Durchsichten: kein Fund (`fd64a69`, 2026-10-17); Fund F68 (`ohne Hash`, 2026-10-17). +- `llama/src/main/java/net/ladenthin/llama/loader/LlamaLoader.java` · `35d2521` · 2026-10-17 · kein Fund — Durchsicht „`loader/LlamaLoader.java` ↔ `llama/cmake/jllama-build-info.cmake` (Schnittstelle `jllama-build.txt`/`jllama-files.txt`/`jllama-extras.txt`)“: Der Schreiber erzeugt eine `#`-Kopfzeile, LF-Zeilenenden und `key=value` mit genau den Schlüsseln, die der Loader als `BUILD_KEY_*` kennt (`llama.cpp`, `llama.cpp.commit`, `backend`, `kind`, `classifier`, `jllama.version`, `jllama.commit`, `patches`, `patches.sha256`, `compiler`, `toolkit`, `signed`); `parseBuild` teilt am ersten `=`, so überstehen Werte mit `=` oder Leerzeichen (Compilerkennung), Kommentare und Leerzeilen werden übersprungen, Listen getrimmt. Musterdurchsicht nach F68: keine weitere Stelle wertet nur `loaded` aus. ‖ **Abgelaufen:** Blob am HEAD `988c770`, seitdem 1 Commit an der Datei, darunter der Fix F239. Frühere Durchsichten: Fund F09 (`35d2521`, 2026-10-17). +- `llama/src/main/java/net/ladenthin/llama/loader/OfflineModelGuard.java` · `658e69a` · 2026-10-17 · Fund F56 — nicht darstellbarer Pfad ergab `InvalidPathException`. Durchsicht der übrigen `Paths.get`-Stellen: `RpcServer.Options.parse` (`--cache`) wirft dieselbe Ausnahme, die dort als `IllegalArgumentException` der Argumentprüfung passt; `OpenAiServerCli.getModelId` nur bei unsinnigem Modellpfad, den der native Lader ohnehin ablehnt – kein Fund. ‖ **Abgelaufen:** Blob am HEAD `8c94266`, seitdem 1 Commit an der Datei, darunter der Fix F238. +- `llama/src/main/java/net/ladenthin/llama/parameters/ChatRequest.java` · `7ac6e49` · 2026-10-18 · kein Fund — Durchsicht „`parameters/ChatRequest.java` + `FfmNativeServer.java` + `server/NativeServer.java` (Musterdurchsicht nach F134: Javadoc „`null` clears“/„to clear“)“: `ChatRequest.withToolChoice`/`withInferenceCustomizer` bauen das Objekt mit dem neuen Wert neu; `setWorkerCommand` übergibt bei FFM `""` und bei JNI `null` → `""`, beides löscht laut `jllama_api.h`; `JllamaNative.takeStrings` (damals unsicher, 17.; inzwischen F168, behoben), da `server_devices()` nie leer liefert. ‖ **Abgelaufen:** Blob am HEAD `11e0f17`, seitdem 2 Commits an der Datei, darunter der Fix F233. Frühere Durchsichten: Fund F55 (`7ac6e49`, 2026-10-17). +- `llama/src/main/java/net/ladenthin/llama/parameters/CliParameters.java` · `b0428f5` · 2026-10-17 · kein Fund — java-llama.cpp `parameters/CliParameters`: argv-Aufbau (Option + Wert, Flag ohne Wert, `argv[0] = ""`), Floats als `String.valueOf` (`1.0E-5`, von `std::stof` gelesen) korrekt. Randnotiz (niedrig, Sicherheit): `ModelParameters.toString()` gibt den Wert von `--hf-token` (`setHfToken`) im Klartext … ‖ **Abgelaufen:** Blob am HEAD `d299f38`, seitdem 2 Commits an der Datei, darunter der Fix F234. +- `llama/src/main/java/net/ladenthin/llama/parameters/InferenceParameters.java` · `386d73f` · 2026-10-18 · Fund F134 — Durchsicht „`parameters/JsonParameters.java` + `InferenceParameters.java` (alle Wither mit Wächterwert: `null`, leere Sammlung, Logit-Bias-Überschreibung)“: die `null`-Wither löschten nicht; die leeren Sammlungen sind als No-op dokumentiert, die vier Logit-Bias-Setter als gegenseitig überschreibend – beides wie beschrieben. ‖ **Abgelaufen:** Blob am HEAD `50d5fb8`, seitdem 8 Commits an der Datei, darunter die Fixes F180, F183, F202. Frühere Durchsichten: Fund F26 (`386d73f`, 2026-10-17); Fund F69 (`386d73f`, 2026-10-18); kein weiterer Fund (zwei Doku-Notizen) (`386d73f`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/parameters/JsonParameters.java` · `0ebc857` · 2026-10-18 · Fund F134 — Durchsicht „`parameters/JsonParameters.java` + `InferenceParameters.java` (alle Wither mit Wächterwert: `null`, leere Sammlung, Logit-Bias-Überschreibung)“: die `null`-Wither löschten nicht; die leeren Sammlungen sind als No-op dokumentiert, die vier Logit-Bias-Setter als gegenseitig überschreibend – beides wie beschrieben. ‖ **Abgelaufen:** Blob am HEAD `6001eb4`, seitdem 1 Commit an der Datei. Frühere Durchsichten: Fund F38 (`1c4c9f7`, 2026-10-17). +- `llama/src/main/java/net/ladenthin/llama/parameters/ModelParameters.java` · `a84e7da` · 2026-10-18 · Fund F133 — Durchsicht „`parameters/ModelParameters.java` (`setPoolingType` gegen die anderen `UNSPECIFIED`-Setter)“: `UNSPECIFIED` entfernte ein früher gesetztes `--pooling` nicht; sonst kein Setter, der bei einem Wächterwert nur das Setzen überspringt. ‖ **Abgelaufen:** Blob am HEAD `1c7a18a`, seitdem 7 Commits an der Datei, darunter die Fixes F185, F234, F243. Frühere Durchsichten: Fund F13 (`90d097c`, 2026-10-17); kein weiterer Fund (zwei Doku-Notizen) (`90d097c`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/parameters/ParameterJsonSerializer.java` · `408b4ae` · 2026-10-18 · kein Fund — Durchsicht „`parameters/ParameterJsonSerializer.java` (`buildMessages`) ↔ upstream `common_chat_msgs_parse_oaicompat` (`common/chat.cpp:342`, b11639; Schnittstelle Nachrichten-Array ↔ Pflichtfelder)“: Jede Nachricht trägt `role` und `content` (String oder Teile-Array), Werkzeugaufrufe `id`, `type: "function"`, `function.name` und `function.arguments` als String, Werkzeugantworten `tool_call_id`: keine der `throw std::invalid_argument`-Stellen (fehlende Rolle, fehlender Typ/Name, weder `content` noch `tool_calls`, falscher `content`-Typ) ist erreichbar. Teile vom Typ `image_url`/`input_audio` wandelt upstreams Serververarbeitung vorher in Medienmarker; `reasoning_content` früherer Antworten und `name` werden nicht mitgeschickt (beide optional). ‖ **Abgelaufen:** Blob am HEAD `5627e1f`, seitdem 4 Commits an der Datei, darunter die Fixes F202, F203. Frühere Durchsichten: Fund F54 (`408b4ae`, 2026-10-17). +- `llama/src/main/java/net/ladenthin/llama/parameters/RequestField.java` · `dbb8b3b` · 2026-10-17 · kein Fund — java-llama.cpp `parameters/RequestField`: alle `SCHEMA`-Schlüssel gegen `server-schema.cpp` b11639 (64 Felder) abgeglichen – vorhanden, `grammar` als Alias von `json_schema` (`add_alias`). Wird zusätzlich vom C++-Test `test_wire_contracts.cpp` erzwungen. ‖ **Abgelaufen:** Blob am HEAD `c3785fe`, seitdem 2 Commits an der Datei, darunter die Fixes F183, F202. +- `llama/src/main/java/net/ladenthin/llama/parameters/TrainingParameters.java` · `12ecdbf` · 2026-10-18 · Fund F71 — Durchsicht „`parameters/TrainingParameters.java` + `LlamaTrainer.java` ↔ `train_engine.cpp`/`train_params.hpp` (Schnittstelle Trainingskonfiguration über JNI)“: Schlüssel und Vorgaben stimmen (`epochs` 2, `learning_rate` 1e-5, `val_split` 0.05, `n_gpu_layers` −1, `n_batch`/`n_ubatch` 0 = Vorgabe); Java prüft, was die Engine nicht prüft (genau ein Korpus, `epochs` ≥ 1, `val_split` in [0, 1)). Die Übergabe per `GetStringUTFChars` zerbrach Zeichen außerhalb der BMP (F71). ‖ **Abgelaufen:** Blob am HEAD `47dd112`, seitdem 1 Commit an der Datei. Frühere Durchsichten: Fund F01 (`8447e35`, 2026-10-17). +- `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java` · `6ae6dd6` · 2026-10-18 · kein Fund — Durchsicht „`server/AnthropicApiSupport.java` (Inhaltsblöcke, Probe mit 6 Varianten)“: Bild als Base64 (`media_type` übernommen) und als URL, `tool_result` mit Text- und Bildteil → `role: tool` mit Teilen, `thinking` → `reasoning_content`; System-Blöcke werden ohne Trenner verbunden und `document` (PDF) fällt weg – beides genau wie upstream (`server_chat_convert_anthropic_to_oai`, `server-chat.cpp:348`, b11639: `system_content += system_text`, kein `document`-Zweig) ‖ **Abgelaufen:** Blob am HEAD `d86b738`, seitdem 6 Commits an der Datei, darunter die Fixes F172, F183, F202, F221, F223. Frühere Durchsichten: Fund F39 (`4c59b6c`, 2026-10-17); Fund F60 (`b59c4ee`, 2026-10-17); kein Fund (`b59c4ee`, 2026-10-17); kein Fund (`b59c4ee`, 2026-10-17); kein Fund (`b59c4ee`, 2026-10-17); kein Fund (`b59c4ee`, 2026-10-17); kein Fund (Randnotiz) (`b59c4ee`, 2026-10-18); Fund F75 (`ohne Hash`, 2026-10-18); Fund F76 (`8a426d9`, 2026-10-18); Fund F80 (`5682409`, 2026-10-18); Fund F81 (`edd2c9a`, 2026-10-18); Fund F84 (`fc5f49b`, 2026-10-18); Fund F88 (`565f7f8`, 2026-10-18); Fund F90 (`9b64484`, 2026-10-18); kein Fund (`9b64484`, 2026-10-18); Fund F96 (`7376c4a`, 2026-10-18); Fund F107 (`6ae6dd6`, 2026-10-18); kein Fund (`6ae6dd6`, 2026-10-18); kein Fund (Anmerkung) (`6ae6dd6`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/server/AnthropicStreamTranslator.java` · `bd6ac4e` · 2026-10-18 · kein Fund — Durchsicht „`server/AnthropicStreamTranslator.java` (Probe: Text, dann Werkzeugaufruf in drei Argumentstücken, `finish_reason: tool_calls`, Usage-Chunk)“: Ereignisfolge `message_start` → Textblock 0 → Stopp → `tool_use`-Block 1 mit den gesammelten Argumenten als ein `input_json_delta` → Stopp → `message_delta` (`stop_reason: tool_use`, Usage aus dem Usage-Chunk) → `message_stop`; das Sammeln der Argumente bis zum Ende ist spezifikationskonform (Anthropic garantiert keine Stückelung) ‖ **Abgelaufen:** Blob am HEAD `9824b5f`, seitdem 4 Commits an der Datei, darunter die Fixes F183, F221, F222. Frühere Durchsichten: kein Fund (`7f002e9`, 2026-10-17); kein Fund (`7f002e9`, 2026-10-17); Fund F81 (`bd6ac4e`, 2026-10-18); kein Fund (`bd6ac4e`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/server/LlamaModelBackend.java` · `1e88958` · 2026-10-18 · Verdacht notiert — Durchsicht „`server/LlamaModelBackend.java` (`streamCompletions` ↔ upstream `to_json_oaicompat`)“: letzter Chunk ohne `usage`, sonst wie upstream (`finish_reason` `length`/`stop`, `logprobs: null`); Sink-Fehler bricht ab und schließt die Aufgabe über `LlamaIterable.close()` ‖ **Abgelaufen:** Blob am HEAD `052500f`, seitdem 4 Commits an der Datei, darunter die Fixes F177, F186, F232. Frühere Durchsichten: kein Fund (`3d47cac`, 2026-10-17); kein Fund (`3d47cac`, 2026-10-17); kein Fund (Designabweichung notiert) (`3d47cac`, 2026-10-17); kein Fund (Randnotiz) (`ohne Hash`, 2026-10-18); Fund F77 (`ohne Hash`, 2026-10-18); Fund F79 (`ohne Hash`, 2026-10-18); Fund F111 (`1e88958`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/server/NativeServer.java` · `c679619` · 2026-10-17 · Fund F18 — Lebenszyklus (`start`/`isRunning`/`close`) unter `lifecycleLock`, abgesichert durch `NativeServerLifecycleTest` über die Naht `NativeServer.Backend` ‖ **Abgelaufen:** Blob am HEAD `5895cc8`, seitdem 1 Commit an der Datei. Frühere Durchsichten: kein Fund (`ohne Hash`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/server/OaiRerankSupport.java` · `b3c1c31` · 2026-10-18 · Fund F111 — TEI-Format (`texts`, bloßes Array, `return_text`) wie upstream ‖ **Abgelaufen:** Blob am HEAD `27ffd89`, seitdem 5 Commits an der Datei, darunter die Fixes F177, F230, F231, F232. Frühere Durchsichten: kein Fund (`b578b98`, 2026-10-17); kein Fund (`b578b98`, 2026-10-17). ‖ Die Notizen vom 2026-10-17 zu `top_n` („`top_n<=0` = alle“, „`top_n ≤ 0` oder fehlend = alle“) sind seit F177 falsch: `top_n: 0` behält kein Ergebnis, ein negatives `top_n` wird mit 400 abgewiesen, ein fehlendes `top_n` behält alle, seit F230 auch eines, das keine als `int` darstellbare Zahl ist (Wahrheitswert, Zeichenkette, außerhalb des `int`-Bereichs). +- `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java` · `73f11e1` · 2026-10-18 · kein Fund — Durchsicht „`server/OllamaApiSupport.java` (`toOllamaContentLine`/`toOllamaDoneLine` mit Werkzeugaufruf im Stream, Probe)“: Textstücke als eigene Zeilen, Werkzeug-Fragmente erzeugen keine Zeile, die Abschlusszeile trägt die gesammelten `tool_calls` mit als Objekt geparsten Argumenten sowie `prompt_eval_count`/`eval_count`; `done_reason: stop` bei Werkzeugaufrufen wie bei Ollama selbst (der `length`-Fall steht schon als Verdacht) ‖ **Abgelaufen:** Blob am HEAD `8425e69`, seitdem 8 Commits an der Datei, darunter die Fixes F178, F179, F205, F213, F215, F216, F217. Frühere Durchsichten: Fund F37 (`c1840dc`, 2026-10-17); kein Fund (Verdacht notiert) (`c1840dc`, 2026-10-17); Fund F64 (`339b3e0`, 2026-10-17); kein Fund (`ohne Hash`, 2026-10-17); Fund F74 (`122da1d`, 2026-10-18); Fund F75 (`ohne Hash`, 2026-10-18); kein Fund (`ed9aff1`, 2026-10-18); Fund F97 (`6f86c26`, 2026-10-18); Fund F98, F97 (`7e6c0b0`, 2026-10-18); Fund F104 (`a944f9a`, 2026-10-18); Fund F114 (`906cf46`, 2026-10-18); kein Fund nach F114 (`906cf46`, 2026-10-18); Fund F115 (`a058a51`, 2026-10-18); Fund F116 (`73f11e1`, 2026-10-18); kein Fund (`73f11e1`, 2026-10-18); kein Fund (Anmerkung) (`73f11e1`, 2026-10-18); Verdacht notiert (`73f11e1`, 2026-10-18); kein Fund (`73f11e1`, 2026-10-18); kein Fund (`73f11e1`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/server/OpenAiBackend.java` · `3353536` · 2026-10-17 · kein Fund — Naht ohne Logik; der Standard von `streamCompletions` wirft `UnsupportedOperationException`, die `OpenAiCompatServer.streamCompletions` als Server-Fehler-Ereignis im SSE-Strom meldet (Kopfzeilen sind dann schon gesendet), statt die Verbindung abzubrechen. Der einzige Produktions-Backend `LlamaModelBackend` überschreibt sie. ‖ **Abgelaufen:** Blob am HEAD `347617d`, seitdem 1 Commit an der Datei. +- `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java` · `a6ed187` · 2026-10-18 · kein Fund — Durchsicht „`server/OpenAiCompatServer.java` (Zugangsprüfung je Route)“: Schlüssel in konstanter Zeit verglichen (`MessageDigest.isEqual`, `Bearer` und `x-api-key`); `/v1/models`, `/metrics`, `/slots` und alle POST-Routen über `authorized`; `/health`, `/props` und die Ollama-Entdeckungsrouten bewusst offen und kommentiert (nur öffentliche Modelldaten); Größenprüfung erst nach der Zugangsprüfung, ein Unbefugter bekommt also kein 413-Orakel ‖ **Abgelaufen:** Blob am HEAD `0e42edc`, seitdem 13 Commits an der Datei, darunter die Fixes F174, F178, F179, F182, F199, F205, F213, F214, F215, F216, F223. Frühere Durchsichten: Fund F25 (`6bdd3ff`, 2026-10-17); kein Fund (`6bdd3ff`, 2026-10-17); Fund F65 (`09c9640`, 2026-10-17); kein Fund (Designabweichung notiert) (`09c9640`, 2026-10-17); Fund F70 (`ohne Hash`, 2026-10-18); kein Fund (Verdacht notiert) (`ohne Hash`, 2026-10-18); Fund F89 (`bc376ce`, 2026-10-18); Fund F98 (`77913cb`, 2026-10-18); kein Fund (`77913cb`, 2026-10-18); Fund F112 (`e38eb7e`, 2026-10-18); kein Fund nach F114 (`ohne Hash`, 2026-10-18); Fund F117 (`65df73d`, 2026-10-18); Fund F118 (`a6ed187`, 2026-10-18); kein Fund (`a6ed187`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java` · `20548dd` · 2026-10-18 · Fund F123 — Durchsicht „`server/OpenAiRequestMapper.java` (Probe mit 14 Feldvarianten)“: `stop` als String und Array, `max_tokens` als Ganz- und Gleitzahl (als String verworfen), `max_completion_tokens` vorrangig, `seed`/`top_k` als Gleitzahl abgeschnitten, Strafen übernommen; `logprobs` fehlte (F123); `n` und `user` bleiben unbeachtet (`n` ist offener F95-Rest) ‖ **Abgelaufen:** Blob am HEAD `7049036`, seitdem 11 Commits an der Datei, darunter die Fixes F180, F183, F186, F202, F218, F219, F220, F227. Frühere Durchsichten: kein Fund (`5bddfea`, 2026-10-17); kein Fund (`5bddfea`, 2026-10-17); Fund F77 (`9eee221`, 2026-10-18); Fund F79 (`d89f722`, 2026-10-18); Fund F95 (`f10f4c6`, 2026-10-18); Fund F102 (`69becc1`, 2026-10-18); Fund F103 (`530fb40`, 2026-10-18); Fund F109, F103, F102 (`bfa8160`, 2026-10-18); Verdacht notiert (`bfa8160`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/server/OpenAiServerCli.java` · `2003840` · 2026-10-18 · Fund F70 — Durchsicht „`server/OpenAiServerCli.java` (`toServerConfig`) + `OpenAiServerConfig` + `OpenAiCompatServer` (`/api/show`, `/props`) ↔ upstream Slot-Kontext (Schnittstelle beworbene Budgets ↔ tatsächliches Fenster, b11639)“: Kontextlänge in `/api/show` und `/props` = `maxInputTokens + maxOutputTokens`; ohne `-c` gelten die Vorgaben; mit `--parallel N` war es das ganze `-c` statt des Fensters je Slot (F70). ‖ **Abgelaufen:** Blob am HEAD `75d0614`, seitdem 6 Commits an der Datei, darunter die Fixes F174, F185, F201. Frühere Durchsichten: Fund F24, F125 (`c9677a3`, 2026-10-18); kein Fund (`fea260b`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/server/OpenAiServerConfig.java` · `190803b` · 2026-10-18 · Fund F138 — Durchsicht „`server/OpenAiServerConfig.java` (zweite Durchsicht: Builder-Werte gegen ihre Verbraucher in `OpenAiCompatServer`)“: Heartbeat 0 brach jeden Stream. `port` außerhalb des Bereichs scheitert schon in `start()` (InetSocketAddress), `maxRequestBodyBytes` ≤ 0 lehnt jeden Rumpf mit 413 ab – sichtbar und eindeutig, nicht geändert. ‖ **Abgelaufen:** Blob am HEAD `3401489`, seitdem 2 Commits an der Datei, darunter der Fix F174. Frühere Durchsichten: Fund F25 (`7f9c370`, 2026-10-17); Fund F70 (`ohne Hash`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/server/OpenAiSseFormatter.java` · `e5eb40e` · 2026-10-19 · kein Fund — Durchsicht „`server/OpenAiSseFormatter.java` (dritte Durchsicht)“: `ensureUsageCachedTokens` ergänzt nur fehlendes `cached_tokens` und gibt jede andere Form (`usage` null, mit Leerzeichen, als Text im Inhalt, kein Objekt, unlesbar) unverändert weiter; SSE-Rahmen mit Leerzeile, Fehlerobjekt mit `code` null. ‖ **Abgelaufen:** Blob am HEAD `0f04b96`, seitdem 2 Commits an der Datei, darunter die Fixes F174, F186. Frühere Durchsichten: kein Fund (`1a04e66`, 2026-10-17); kein Fund (Verdacht notiert) (`1a04e66`, 2026-10-18); Fund F113 (`e5eb40e`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java` · `12c6846` · 2026-10-18 · kein Fund — Durchsicht „`server/ResponsesApiSupport.java` (`input`-Items, Probe mit 4 Folgen)“: aufeinanderfolgende `function_call`-Items werden zu einer Assistentennachricht mit mehreren `tool_calls` zusammengefasst, eine vorangehende `output_text`-Nachricht wird ihr Text; `function_call_output` als String und als Teilliste → `role: tool`; `developer` bleibt erhalten und wird von upstreams `common/chat.cpp:1336` (`map_developer_role_to_system`, außer GPT-OSS) zu `system` ‖ **Abgelaufen:** Blob am HEAD `bc5484d`, seitdem 10 Commits an der Datei, darunter die Fixes F172, F173, F224, F225, F226, F227, F228, F229. Frühere Durchsichten: Fund F17 (`ac50ae4`, 2026-10-17); Fund F61 (`1365c84`, 2026-10-17); Fund F63 (`b755abd`, 2026-10-17); kein Fund (`b755abd`, 2026-10-17); kein Fund (`b755abd`, 2026-10-17); kein Fund (Randnotiz) (`b755abd`, 2026-10-18); Fund F75 (`446fa5f`, 2026-10-18); Fund F82 (`2a6035a`, 2026-10-18); Fund F85 (`a2333b5`, 2026-10-18); Fund F86 (`210b00c`, 2026-10-18); Fund F87 (`32fe049`, 2026-10-18); Fund F89 (`1fd936a`, 2026-10-18); Fund F91 (`945d6fd`, 2026-10-18); Fund F99 (`2358a6a`, 2026-10-18); Fund F105 (`886b3cb`, 2026-10-18); Fund F106 (`1724b42`, 2026-10-18); Fund F108 (`e44a70b`, 2026-10-18); Fund F109, F108, F106 (`12c6846`, 2026-10-18); kein Fund (`12c6846`, 2026-10-18); kein Fund (Anmerkung) (`12c6846`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/server/ResponsesStreamTranslator.java` · `6d97f3a` · 2026-10-18 · kein Fund — Durchsicht „`server/ResponsesStreamTranslator.java` (Probe: Text, Werkzeugaufruf, `tool_calls`, Usage)“: Nachrichten-Item wird vor dem Werkzeugaufruf vollständig geschlossen (`output_text.done`, `content_part.done`, `output_item.done`), dann `function_call`-Item mit `call_id`, Name und vollständigen Argumenten (`added` → `function_call_arguments.done` → `done`), dann `response.completed` mit beiden Items; Folge­nummern lückenlos; Codex liest genau `output_item.done`, das kommt für beide Items ‖ **Abgelaufen:** Blob am HEAD `bb101a1`, seitdem 1 Commit an der Datei, darunter der Fix F229. Frühere Durchsichten: kein Fund (`51665a1`, 2026-10-17); kein Fund (`51665a1`, 2026-10-17); Fund F83 (`007523f`, 2026-10-18); Fund F117 (`6d97f3a`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/server/RouterClient.java` · `f23050e` · 2026-10-19 · Fund F143 — Durchsicht „`server/RouterClient.java` (`awaitModelLoaded`, Fristgrenzen)“: bei Frist 0 keine einzige Abfrage. ‖ **Abgelaufen:** Blob am HEAD `02a6154`, seitdem 2 Commits an der Datei, darunter der Fix F175. Frühere Durchsichten: Fund F06 (`3f14aec`, 2026-10-17); Fund F68 (`5396c0a`, 2026-10-17); kein Fund (`5396c0a`, 2026-10-18). +- `llama/src/main/java/net/ladenthin/llama/value/RouterModel.java` · `878a722` · 2026-10-18 · kein Fund — Statusnamen decken `server_model_status_to_string` vollständig ab, `unknown` → `UNKNOWN` ‖ **Abgelaufen:** Blob am HEAD `12d852c`, seitdem 1 Commit an der Datei, darunter der Fix F175. Frühere Durchsichten: kein Fund (`878a722`, 2026-10-17); Fund F68 (`ohne Hash`, 2026-10-17). +- `llama/src/main/java/net/ladenthin/llama/value/ServerMetrics.java` · `2c10c02` · 2026-10-18 · kein Fund — Durchsicht „`value/ServerMetrics.java` ↔ `server_metrics_to_json` (`json_helpers.hpp`)“: alle 22 gelesenen Schlüssel werden auf der C++-Seite geschrieben (Abgleich per grep), Zeiten in Millisekunden (`to_ms` aus Mikrosekunden), Raten und `getDraftAcceptanceRate` gegen Division durch 0 geschützt, Argumentreihenfolge des `Timings`-Konstruktors stimmt. ‖ **Abgelaufen:** Blob am HEAD `eff2577`, seitdem 1 Commit an der Datei, darunter der Fix F57. Frühere Durchsichten: kein Fund (`2c10c02`, 2026-10-17). +- `llama/src/main/java/net/ladenthin/llama/value/ToolCall.java` · `acce5a3` · 2026-10-17 · kein Fund — unveränderlicher Träger (id, name, Argumente als JSON-Text); `equals`/`hashCode` über alle drei Felder. ‖ **Abgelaufen:** Blob am HEAD `81208e8`, seitdem 1 Commit an der Datei. + +## Musterdurchsichten und Schnittstellen ohne Blob-Bindung + +Diese Durchsichten galten einem Muster oder einer Schnittstelle über mehrere Dateien und belegen keine einzelne +Datei; ein angegebener Hash ist der Stand der Hauptdatei zur Zeit der Durchsicht und wird nicht nachgeführt. + +- Musterdurchsicht nach F70 (Kontextlänge aus `n_ctx` abgeleitet) über `llama/src/main/java`, `llama-langchain4j`, `llama-kotlin`, `llama-ffm` · – · 2026-10-18 · kein weiterer Fund (Verdacht notiert) — Einzige Ableitung ist `OpenAiServerCli.toServerConfig`; `OpenAiCompatServer` meldet in `/api/show` und `/props` nur `maxInputTokens + maxOutputTokens`; `RouterModel.contextLength` und `ModelMeta` geben upstreams Werte unverändert weiter. Ohne `-c` werden feste Vorgaben beworben (offener Verdacht). +- Musterdurchsicht nach F71 (Modified UTF-8 an der JNI-Grenze) über `llama/src/main/cpp/*` als Vertragsreferenz · – · 2026-10-18 · kein Fund im Ziel (Verdacht außerhalb notiert) — Eingehend nutzt nur `train_engine.cpp` `GetStringUTFChars` (behoben über F71 auf der Java-Seite); `jllama.cpp` und `native_server.cpp` gehen ausdrücklich über `String.getBytes("UTF-8")`. Ausgehend gehen Nutzdaten über `utf8_to_jstring_impl`; nur `jllama_build_info` (ASCII), Gerätenamen in `rpc_bridge.cpp` und die Fehlermeldungen nutzen `NewStringUTF`/`ThrowNew` (offener Verdacht, C++). +- `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java` + `AnthropicApiSupport.java` + `AnthropicStreamTranslator.java` + `ResponsesApiSupport.java` (Musterdurchsicht leere Werkzeug-Argumente) · – · 2026-10-18 · kein Fund (Verdacht ergänzt) — Rückweg OpenAI → Ollama/Anthropic wandelt `""` und Unparsbares in ein leeres Objekt (F37/F38); Hinweg Responses/Anthropic → OpenAI kann `""` weitergeben (offener Verdacht „Leere Werkzeug-Argumente“). +- `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java` + `ResponsesStreamTranslator.java` + `OpenAiCompatServer.java` (`withUsageChunk`) ↔ upstream `to_json_oaicompat_resp` (`server-task.cpp:576`, b11639; Schnittstelle Usage) · – · 2026-10-18 · kein Fund (Verdacht ergänzt) — `input_tokens` = alle Prompt-Token inkl. Cache, `output_tokens`, `total_tokens` = Summe, `input_tokens_details.cached_tokens` wie upstream, blockierend und gestreamt; die Anthropic- und Responses-Streams fordern `include_usage` an. Der Ollama-Stream nicht (Nachtrag zum `done_reason`-Verdacht). +- Musterdurchsicht nach F72 (Rekursion auf Fremdeingabe ohne Tiefengrenze) über alle Ziele der drei Repos · – · 2026-10-18 · kein weiterer Fund — Heuristik: Methoden, die ihren eigenen Namen im Rumpf aufrufen; fast alles Delegation an gleichnamige Methoden anderer Objekte. Echte Rekursion auf Fremdeingabe nur in BAF `SeparatorFormat.splitRecursive` (Tiefe = Anzahl der Trennzeichen, fest) und eben `GgufInspector.readArray` (F72). Jackson begrenzt die Schachtelungstiefe beim Parsen selbst (`StreamReadConstraints`), die Fehler landen dort als `JsonProcessingException`. +- Musterdurchsicht nach F77 (Felder, die ein Übersetzer oder der Mapper still verwirft) über `OllamaApiSupport`, `AnthropicApiSupport`, `ResponsesApiSupport`, `OpenAiRequestMapper` · `ed9aff1` · 2026-10-18 · Fund F78 — Ollama `think` fehlte (F78); Antwortseite ohne `message.thinking` (Verdacht). Weiter offen: der gestreamte `/v1/completions`-Pfad baut `InferenceParameters` über den Mapper, der nicht gestreamte reicht den Body unverändert durch – dieselbe Anfrage verliert gestreamt Felder wie `suffix`, `min_p`, `grammar`, `logprobs` (nächstes Ziel). +- `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java` ↔ upstream `oaicompat_chat_params_parse` (`server-common.cpp:1270`–`1470`, b11639; Abweisungen und Sonderfelder) · – · 2026-10-18 · kein Fund (Verdacht ergänzt) — `tool_choice` als Objekt fällt bei upstream ebenso auf `auto` zurück (`json_value(…, std::string)`); `stop` als String wird zur Liste wie bei uns; die Abweisungen (Grammatik + Werkzeuge, `response_format`-Typ, `messages` fehlt/leer, `logprobs` + Werkzeuge im Stream) treffen bei uns über die native Seite ein. `n` fehlt (Verdacht „Feldverlust“ ergänzt). +- Musterdurchsicht `awaitTermination`/`shutdownNow` nach F66 (BAF `KeyProducerJavaSocket.interrupt`, `Finder.shutdownAndAwaitTermination`; java-llama.cpp `OpenAiCompatServer.close`) · – · 2026-10-18 · kein Fund — Socket-Producer schließt die Verbindungen nach `shutdownNow`, das blockierte `read` endet dadurch; `Finder` reicht nach Ablauf der Wartezeit `interrupt()` an alle Producer weiter, bevor der Consumer gestoppt wird (der Consumer-Fall ist F66); `OpenAiCompatServer.close` gibt das native Modell nicht frei, laufende Streams enden über den Schreibfehler des geschlossenen Sockets und den Abbruch in `streamChatCompletion`. +- Nachprüfung der Kommentare neben den Fixes dieser Sitzung (Erkenntnis aus F110): streambuffer `StreamBuffer.java` (`trim`-Erwähnungen außer F110 zutreffend), java-llama.cpp `OpenAiRequestMapper`/`ResponsesApiSupport` (zwei veraltete Kommentare zu `tool_choice`, als Nachtrag zu F109 notiert) · – · 2026-10-18 · kein neuer Fund +- Schnittstelle `llama-langchain4j` → `value.ChatMessage` → `ParameterJsonSerializer.buildMessages` (Werkzeugergebnis- und Aufrufkette) · – · 2026-10-18 · kein Fund — `tool_call_id` nur bei vorhandener Id gesetzt, `tool_calls` mit `id`/`type`/`function.name`/`function.arguments`; die Pflichtfeldprüfung gegen upstreams `common_chat_msgs_parse_oaicompat` ist schon dokumentiert (Zeile zu `buildMessages`), nichts Neues +- `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/*.java` ↔ langchain4j-core 1.22.0 (Musterdurchsicht nach F128: Vorbedingungen der Fremdklassen, `javap -c`) · `f7f3f0f` · 2026-10-18 · kein Fund — `AiMessage(String)` `ensureNotNull` (`ChatResponse.getFirstContent()` liefert nie null, `Optional.map` macht aus einem null-Inhalt ""), `AiMessage(List)` `ensureNotEmpty` (nicht benutzt, der Builder prüft nichts), `PartialThinking` `ensureNotEmpty` (vorher auf leer geprüft), `CompleteToolCall`/`PartialToolCall` `ensureNotNegative(index)` (Index aus upstreams `tool_call_index`, nie negativ), `ChatResponse` `ensureNotNull(aiMessage)` (immer gesetzt), `Embedding(float[])` `ensureNotNull` (natives `embed` liefert ein Array), `ToolExecutionRequest`/`TokenUsage` ohne Prüfungen. `llama-kotlin` baut keine Fremdobjekte mit Vorbedingungen. +- `llama/src/test/java/net/ladenthin/llama/server/RouterModeIntegrationTest.java` (`discoverModelName`) ↔ upstream `common_preset_context::load_from_models_dir` (`common/preset.cpp:448`–`450`, b11639) · – · 2026-10-21 · kein Fund (Test-Nachtrag, Branch-Review 2026-10-20, Fund JF-8) — Der Router nennt ein Einzeldatei-Modell im `--models-dir` nach seinem Dateinamen ohne jedes `.gguf` (`string_replace_all(name, ".gguf", "")`); der Test suchte stattdessen eine Id, die `"Qwen3"` enthält, und fand mit einem anderen Modell über `net.ladenthin.llama.reasoning.model` nichts. Jetzt leitet `expectedModelId` die Id genauso aus dem Namen des Symlinks ab, den der Test selbst anlegt, und `discoverModelName` verlangt Gleichheit; die Meldung nennt die erwartete Id. Für das CI-Modell `Qwen3-0.6B-Q4_K_M.gguf` ist das `Qwen3-0.6B-Q4_K_M`, dieselbe Id wie bisher. Rot-Grün per Konfiguration mit dem Winzmodell: vorher `RouterModeIntegrationTest.setup:113->discoverModelName:191 GET /models did not list the symlinked model: [tiny [unloaded]]`, danach `Tests run: 2, Failures: 0, Errors: 0, Skipped: 0` (Worker-JVM geladen, Chat über den Router beantwortet); der Testinhalt ist unverändert. +- Die FIM-Tests `ChatScenarioTest#testHandleInfillDirect`, `ErrorHandlingTest#testHandleInfillMissingInputSuffix`, `OpenAiServerCompletionIntegrationTest#infillReturnsContent` und `#ollamaGenerateWithSuffixUsesInfill` ↔ `jllama_engine.cpp` `infill` (die FIM-Prüfung vor der Feldprüfung) · – · 2026-10-21 · kein Fund (Test-Nachtrag, Branch-Review 2026-10-20, Fund JF-9; Verdacht ergänzt) — Neue Annahme `FimAssumptions.assumeFimTokens(model)` am Anfang jedes der vier Tests: ein Infill ohne `input_prefix` scheitert bei einem Modell mit FIM-Tokens an dem fehlenden Feld (`InvalidRequestException`, nichts wird erzeugt) und bei einem ohne mit genau `Model does not support fill-in-the-middle infill` (nur diese Meldung wird zum Abbruch, jede andere Antwort ist ein Fehler). Die GGUF-Metadaten taugen nicht als Quelle: CodeLlamas GGUF nennt keinen `tokenizer.ggml.fim_*`-Schlüssel, upstream findet die Tokens über ihren Text (`"▁
"` usw., `llama-vocab.cpp:2867`ff., b11639); deshalb fragt die Annahme die native Prüfung selbst. Rot-Grün per Konfiguration: mit dem Winzmodell vorher `Tests run: 1, Failures: 0, Errors: 1` (ChatScenarioTest), `Tests run: 1, Failures: 0, Errors: 1` (ErrorHandlingTest), `Tests run: 2, Failures: 2, Errors: 0` (OpenAiServerCompletionIntegrationTest), nachher `Tests run: 1, Failures: 0, Errors: 0, Skipped: 1`, `Tests run: 1, Failures: 0, Errors: 0, Skipped: 1`, `Tests run: 2, Failures: 0, Errors: 0, Skipped: 2`; mit einer Variante des Winzmodells, deren Metadaten drei FIM-Token-Ids nennen, laufen alle vier und sind grün (`Skipped: 0`), die Annahme springt bei FIM-Tokens also nicht an. `LlamaModelTest#testCompleteInfillCustom`, den der Brief als fünften nennt, erreicht die FIM-Prüfung nicht und bekommt die Annahme nicht: er scheitert aus einem anderen Grund (Verdacht unter „Offene Verdachte ohne Fund-ID“).
+- `LlamaModelTest#testCompleteWithCancellationToken` und `#testCompleteAsyncCancelPropagates` ↔ `LlamaModel.complete(InferenceParameters, CancellationToken)` / `completeAsync(InferenceParameters, CancellationToken)` · – · 2026-10-21 · kein Fund (Test-Nachtrag, Branch-Review 2026-10-20, Fund JF-10) — Beide hingen vom Tempo des Modells ab (200 ms Schlaf, dann der Abbruch; mit dem Winzmodell war die Generierung bis dahin oft schon fertig: „future should report cancelled ==> expected:  but was: “ bzw. „token should be reset after call returns ==> expected:  but was: “). Jetzt laufen sie gegen eine eigene Instanz des Textmodells mit `enableContextShift()` (Kontext 128) und einer Anfrage ohne Token-Limit mit `ignore_eos` (Muster von J2-7), so dass die Generierung nur durch den Abbruch endet; abgebrochen wird, sobald ein Slot erzeugt (Abfrage der Metriken statt Schlaf), und geprüft wird, dass `complete` binnen 30 s mit einem Ergebnis zurückkehrt und das Token zurückgesetzt hat bzw. die Future abgebrochen meldet, und in beiden Fällen, dass danach kein Slot mehr arbeitet. Das deckt die Javadoc beider Methoden: Rückkehr mit dem bis dahin erzeugten Text, Zurücksetzen des Tokens, `future.cancel(true)` ruft über `whenComplete` `cancel()` des Tokens auf und beendet die Schleife. Rot-Grün: ohne `token.cancel()` bzw. `future.cancel(true)` im Test rot (`testCompleteWithCancellationToken:328 » Timeout`, `testCompleteAsyncCancelPropagates:420 future should report cancelled ==> expected:  but was: `); mit vorübergehend abgeschaltetem Abbruch in `LlamaModel` (kein `token.cancel()` im `whenComplete`, keine Prüfung des Tokens in der Schleife) rot (`» Timeout` bzw. `awaitProcessingSlots:466 a slot is still processing within 30000 ms`); unverändert grün in sechs Läufen hintereinander mit dem Winzmodell (je `Tests run: 2, Failures: 0, Errors: 0, Skipped: 0`).
+- `LlamaModelTest#testCompleteGrammar` / `#testGenerateGrammar` und `ChatAdvancedTest#testMessagesInGenerate` ↔ upstream `server-context.cpp:3564` (leerer Prompt, b11639) · – · 2026-10-21 · kein Fund in den Grammar-Tests (Test-Nachtrag, Branch-Review 2026-10-20, Fund JF-11); Verdacht ergänzt für `testMessagesInGenerate` — Gemessen statt angenommen: nicht frühes EOS. Der Prompt `""` ist auf einem Modell ohne BOS kein einziges Token; upstream gibt den Slot dann frei und antwortet leer (`"empty prompt - releasing slot"`: `content` `""`, `tokens_evaluated` 0, `tokens_predicted` 0, `stop_type` `none`), ohne zu sampeln, die Grammatik wird also nie berührt; mit dem Prompt `"x"` liefert dasselbe Winzmodell unter der Grammatik `aababbaabb` (`stop_type` `limit`). Nur CodeLlama, das ein BOS setzt, erzeugte etwas. Die Grammar-Tests nutzen jetzt den Prompt `GRAMMAR_PROMPT` und `ignore_eos` (die Grammatik lässt EOS zu, sobald ein Buchstabe stand, und das Winzmodell hängt den Text seines EOS an, weil es dort ein gewöhnliches Byte-Token ist: `abbba>`); die Zusicherung (`[ab]+`, 1 … `nPredict + 1` Tokens) ist unverändert. Rot-Grün per Konfiguration: mit dem Winzmodell vorher `testCompleteGrammar:228  doesn't match [ab]+ ==> expected:  but was: ` und `testGenerateGrammar:185 expected:  but was: `, danach dreimal `Tests run: 2, Failures: 0, Errors: 0, Skipped: 0`; ohne die Grammatik rot (`NYq:_EVhP; doesn't match [ab]+`), sie wird also geprüft. `testMessagesInGenerate` bleibt unverändert rot (`generate() with messages must produce output`): die Ursache ist ein Produktionsverdacht, kein Testproblem (unter „Offene Verdachte ohne Fund-ID“).
+- `llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/AgentOptions.java` (`withWorkspace`) · 8d72fbb · 2026-10-21 · kein Fund (Test-Nachtrag, Branch-Review 2026-10-20/21, Brief JR-15) — Der Befund des Briefs (`withWorkspace` kopiere nur `baseUrl`, `modelPath` und `workspace`) gilt am HEAD nicht mehr: die Methode kopiert alle 26 Felder ausdrücklich (seit dem Merge von #494), `AcpServer` bekommt also alle Einstellungen der Kommandozeile. Offen war nur, dass ein später hinzugefügtes Feld, das die Kopie vergisst, unbemerkt bliebe. Neu `AgentOptionsTest#withWorkspaceKeepsEveryOtherOption`: setzt reflektiv jedes Instanzfeld auf einen Wert, den kein Parse erzeugt (je Typ; ein unbekannter Typ lässt den Test mit einem Hinweis scheitern), ruft `withWorkspace` und verlangt jedes Feld außer `workspace` gleich. Gegenprobe rot mit einer Kopie ohne `b.acp = acp;`: „AgentOptionsTest.withWorkspaceKeepsEveryOtherOption:315 acp Expected: is  but: was “; am HEAD grün, `AgentOptionsTest` 18/18. Keine Fund-Id, da keine Produktionsänderung nötig war.
+- `llama/src/test/cpp/test_rpc.cpp` (`RpcClient.AStaleServerIsKeptOutOfALaterLoadThatDidNotAskForIt`) ↔ `llama/src/main/cpp/rpc_support.hpp` (`prepare_argv`, `device_override`) · – · 2026-10-21 · kein Fund (Test-Nachtrag, Branch-Review 2026-10-20/21, Brief JR-16) — Ursache bestimmt: ggmls Geräteregister ist prozessweit und kennt kein Abmelden, der erste Durchlauf hinterlässt also sein RPC-Gerät; der zweite wählt seinen Port über die Uhr neu (`loopback_server`), so dass das alte Gerät für ihn ein nicht angeforderter, veralteter Server ist, und `prepare_argv` hängt dann richtigerweise auch an die Ladung *mit* `--rpc` eine `--device`-Liste an. Der Test nahm an, die anfordernde Ladung bleibe immer unberührt. Er hält jetzt fest, welche RPC-Geräte vor ihm registriert waren: ohne solche bleibt die Ladung unberührt (wie bisher), mit solchen muss die angehängte Liste das eigene Gerät nennen und keines der veralteten; alle übrigen Zusicherungen sind unverändert. Die Produktion war richtig, keine Fund-Id. Rot-Grün: vorher `jllama_test --gtest_filter='RpcClient.AStale*' --gtest_repeat=2` in Durchlauf 2 rot (`test_rpc.cpp:342: Failure Value of: jllama::rpc::has_device_option(with_rpc) Expected: false`), nachher `--gtest_repeat=3` dreimal `[  PASSED  ] 1 test.` und `--gtest_filter='Rpc*' --gtest_repeat=2` zweimal `[  PASSED  ] 28 tests.`
+- `llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/RetryPolicyWireTest.java` (neu) ↔ Atmosphere 4.0.75 `OpenAiCompatibleClient.sendWithRetry` / `ProviderErrorClassifier.fromHttpStatus` / `RetryPolicy` (gelesen per `javap -c`) · – · 2026-10-21 · kein Fund (Test-Nachtrag, Branch-Review 2026-10-20/21, Brief JR-17) — Die übrigen Wire-Tests laufen mit `RetryPolicy.NONE`, der Agent selbst mit `RetryPolicy.DEFAULT` (drei Wiederholungen ab 1 s; wiederholbar sind die Fehlertypen `rate_limit`, `timeout`, `server_error`, `unavailable`, also 429, 408, 500, 502, 503); ein `4xx` außerhalb davon wird von Atmosphere nicht wiederholt, das war ungetestet. Der neue Test fährt einen eigenen JDK-`HttpServer` auf Loopback, der jede Chat-Anfrage mit demselben Status beantwortet und zählt, und eine Policy mit DEFAULTs Fehlertypen und Verzögerung null (zwei Wiederholungen; kein Warten, die Zählung ist exakt): `aBadRequestIsSentExactlyOnce` (400 → genau eine Anfrage, die Runde endet mit dem Fehler) und `anUnavailableServerIsAskedAgainUntilTheRetriesAreUsedUp` (503 → drei Anfragen, danach endet die Runde mit dem Fehler statt zu hängen). Grün: `Tests run: 2, Failures: 0, Errors: 0, Skipped: 0`. Gegenproben rot: derselbe Test mit 429 statt 400 „Expected: is <1> but: was <3>“ (die Zählung sähe eine Wiederholung), mit `RetryPolicy.NONE` statt der Policy „Expected: is <3> but: was <1>“. Keine Produktionsänderung, keine Fund-Id.
+- Die 16 modellgebundenen Tests (`LlamaEmbeddingsTest` vier, `LlamaModelTest#testEmbedding`, `#testTokenization`, `#testVocabOnly`, `#testChatCompleteMultiTurn`, `#testSessionMultiTurn`, `QuantizerIntegrationTest#quantize_producesLoadableModel` und `#quantize_requantizeWithoutOptIn_throws`, `ReasoningBudgetTest#testThinkingDefault_reasoningContentAndAnswerPresent`, `SessionConcurrencyTest#testConcurrentSendProducesAlternatingTranscript`, `ChatScenarioTest#testChatCompleteMultiTurnThreeTurns`, `SessionForkRewindIntegrationTest`s Fork- und Rewind-Fall) ↔ neues `llama/src/test/java/net/ladenthin/llama/ModelAssumptions.java` · – · 2026-10-21 · kein Fund (Test-Nachtrag, Branch-Review 2026-10-20/21, Brief JR-18) — Der Brief nennt 17; die Liste in TODO.md zählt 16, und mit dem Winzmodell waren genau 16 rot. Jeder Test prüft jetzt vor seiner unveränderten Zusicherung die Modelleigenschaft, die sie voraussetzt, am geladenen Modell und überspringt sich sonst mit Begründung (`ModelAssumptions`): Embedding-Dimension (`ModelMeta.getNEmbd()` = 4096), SentencePiece mit Leerzeichen-Präfix (`getVocabType()` = 1 = `LLAMA_VOCAB_TYPE_SPM` und `tokenizer.ggml.add_space_prefix` nicht `false`), ein `` im Chat-Template des Modells, und für die Mehrzug-Tests, dass das ganze Gespräch, das der Test senden wird, mit dem Template des Modells gerendert und mit seinem Tokenizer gezählt, plus `n_predict` je Antwort in den Kontext eines Slots passt (`getMetricsTyped().getSlotMetrics()`); der Quantizer-Test lädt das Eingabemodell und verlangt ≥ 100 M Parameter (Q4_0 hat 4,5 Bit je Gewicht, 100 M ergeben ~56 MB > 10 MB) bzw. einen quantisierten Dateityp (`getFtype()` nicht `all F32`/`F16`/`BF16`, so wie `llama_model_ftype_name` sie nennt; upstream verweigert das Requantisieren nur für quantisierte Tensoren, `llama-quant.cpp:1311`). Rot vorher mit dem Winzmodell (`-Dnet.ladenthin.llama.{text,draft,reasoning}.model=tiny.gguf`, sieben Klassen): `Tests run: 104, Failures: 11, Errors: 5, Skipped: 6`, u. a. `LlamaEmbeddingsTest.testEmbedMeanPooling:100 expected: <4096> but was: <32>`, `LlamaModelTest.testTokenization:667 expected: < Hello, world!> but was: `, `QuantizerIntegrationTest.quantize_producesLoadableModel:50 quantized 135M model should be well above 10 MB`, `quantize_requantizeWithoutOptIn_throws:69 Expected net.ladenthin.llama.exception.LlamaException to be thrown, but nothing was thrown.`, `ReasoningBudgetTest…:112 reasoning_content should be non-empty`, `LlamaModelTest.testSessionMultiTurn:527 ? InvalidRequest request (190 tokens) exceeds the available context size (128 tokens)`; nachher `Tests run: 104, Failures: 0, Errors: 0, Skipped: 22` (genau die 16 mehr), mit Gründen wie „needs CI's CodeLlama-7B: the test asserts embedding dimension 4096, this model has 32“, „… this model has vocab type 2“, „… this model has 37024 parameters“, „… file type is (guessed) all F32“, „the conversation does not fit this model's slot context: 182 prompt tokens + 16 for the answers > 128“. Die Bedingungen gelten für die CI-Modelle, belegt statt geraten: CodeLlama-7B hat `vocab_type` 1, `n_embd` 4096, `n_ctx_train` 16384 (die Metadaten in `ModelMetaTest`, `n_vocab` 32016, `n_params` 6738546688, `size` 2825274880 = `codellama-7b.Q2_K.gguf`), und diese Tests sind in CI mit ihren Zusicherungen grün; die Gesprächslängen mit upstreams Vokabular-GGUFs gezählt (`models/ggml-vocab-llama-spm.gguf`, dasselbe SentencePiece-Vokabular ohne die 16 FIM-Tokens von CodeLlama, das ChatML-Ersatztemplate, das upstream ohne eigenes Template nimmt; `models/ggml-vocab-qwen2.gguf` mit `models/templates/Qwen-Qwen3-0.6B.jinja`): `testChatCompleteMultiTurn` 76 + 10 ≤ 128, `testSessionMultiTurn` 92 + 16 ≤ 128, `ChatScenarioTest` 103 + 30 ≤ 512, `SessionConcurrencyTest` 160 + 8 ≤ 2048, Fork/Rewind mit Qwen3 33 + 48 ≤ 1024 (bytegenau gezählt mit dem Winzmodell sogar 171 + 48); Qwen3s Template enthält `` (dreimal in der Upstream-Datei); AMD-Llama-135m hat ~134 M Parameter und ist Q2_K (und `quantize_requantizeWithoutOptIn_throws` ist in CI grün, die Eingabe also quantisiert). `.github/verify-test-counts.sh` bleibt unberührt: in CI überspringt keiner dieser Tests. Dabei aufgefallen: `applyTemplate` und `complete` auf einem mit `--vocab-only` geladenen Modell beenden die JVM (Verdacht unter „Offene Verdachte ohne Fund-ID“ in `BUGHUNT-FOUND.md`).
+- `llama/src/test/java/net/ladenthin/llama/MultimodalIntegrationTest.java` (`#imageBytesAffectTheAnswer`) ↔ `ModelAssumptions#assumeTrainedVisionModel` · `fcb0e37` · 2026-10-21 · kein Fund (Test-Nachtrag, Branch-Review 2026-10-20/21, Brief JR-24) — Folgepunkt des Prüfers aus dem Durchgang 2026-10-21 (Brief JR-24), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. Die JR-18-Umsetzung (`5bd74b9`, `ModelAssumptions`) hatte die Klasse nicht erfasst. Der Test prüft, dass das Modell die Farbe eines Bildes nennt, eine Eigenschaft von CIs SmolVLM-500M-Instruct, nicht der Bibliothek; mit dem Winzmodell und dem Winz-Projektor der C++-Fixture vorher rot („MultimodalIntegrationTest.imageBytesAffectTheAnswer:222 red image must be identified as red; got: +p\fff[~ ==> expected:  but was: “). Jetzt liest `assumeTrainedVisionModel` vor der unveränderten Assertion, ob ein Projektor geladen ist und das Sprachmodell mindestens 100 M Parameter hat (SmolVLM-500M dekodiert mit SmolLM2-360M, rund 360 M; der Vision-Encoder steckt im mmproj und zählt nicht), sonst übersprungen mit Grund („needs CI's SmolVLM-500M: … this model has 37024 parameters“); mit dem Winzpaar 6 Tests, 1 übersprungen, 0 Fehler. In CI hält die Bedingung, nichts wird übersprungen.
diff --git a/BUGHUNT-FOUND.md b/BUGHUNT-FOUND.md
new file mode 100644
index 000000000..c3f8b88e1
--- /dev/null
+++ b/BUGHUNT-FOUND.md
@@ -0,0 +1,2562 @@
+
+
+
+# Bug-Hunt: gefundene Fehler — java-llama.cpp
+
+Fortlaufendes Fundbuch der Fehlersuche auf dem Arbeitsbranch `ccr-90fc5f6a-vs4s0f`. Jeder Fund hat eine
+globale ID (`Fnn`, über alle drei Repos eindeutig), Datei:Zeile, Schwere, Beschreibung, Reproduktion (Testname),
+Status und Commit. Neue Funde werden **unten angehängt**; die nächste freie ID ist die höchste ID in allen drei
+Repos plus eins. Geprüfte Klassen ohne Fund stehen in [`BUGHUNT-CHECKED.md`](BUGHUNT-CHECKED.md).
+
+## Übersicht
+
+- [F01](#f01) — niedrig — *gefixt*
+- [F05](#f05) — niedrig — *gefixt*
+- [F06](#f06) — mittel — *gefixt*
+- [F07](#f07) — niedrig — *gefixt*
+- [F09](#f09) — hoch — *gefixt*
+- [F12](#f12) — mittel — *gefixt*
+- [F13](#f13) — mittel — *gefixt*
+- [F16](#f16) — mittel — *gefixt (JNI; die FFM-Hälfte als F198)*
+- [F17](#f17) — mittel — *gefixt*
+- [F18](#f18) — mittel — *gefixt (Fix `5df523f`; roter Regressionstest nachgereicht, bis dahin „nur beschrieben“, siehe `d5c2622`)*
+- [F24](#f24) — mittel — *gefixt*
+- [F25](#f25) — mittel — *gefixt*
+- [F26](#f26) — mittel — *gefixt*
+- [F28](#f28) — mittel — *gefixt*
+- [F29](#f29) — mittel — *gefixt*
+- [F30](#f30) — mittel — *gefixt*
+- [F36](#f36) — mittel — *gefixt*
+- [F37](#f37) — niedrig — *gefixt*
+- [F38](#f38) — mittel — *gefixt*
+- [F39](#f39) — niedrig — *gefixt*
+- [F53](#f53) — niedrig — *gefixt*
+- [F54](#f54) — niedrig — *gefixt*
+- [F55](#f55) — niedrig — *gefixt*
+- [F56](#f56) — niedrig — *gefixt*
+- [F57](#f57) — niedrig — *gefixt*
+- [F59](#f59) — niedrig — *gefixt*
+- [F60](#f60) — niedrig — *gefixt*
+- [F61](#f61) — niedrig — *gefixt*
+- [F62](#f62) — niedrig — *gefixt als F94 (Duplikat: derselbe Befund wurde in der Musterdurchsicht nach F60/F61 noch einmal aufgenommen und dort behoben; Nachtrag 2026-10-19)*
+- [F63](#f63) — niedrig — *gefixt*
+- [F64](#f64) — mittel — *gefixt*
+- [F65](#f65) — niedrig — *gefixt*
+- [F67](#f67) — niedrig — *gefixt (zuerst Dokumentation, seit 2026-10-19 Patch 0021, siehe Nachtrag)*
+- [F68](#f68) — niedrig — *gefixt*
+- [F69](#f69) — niedrig — *gefixt (Dokumentation)*
+- [F70](#f70) — mittel — *gefixt*
+- [F71](#f71) — niedrig — *gefixt*
+- [F72](#f72) — niedrig — *gefixt*
+- [F74](#f74) — mittel — *gefixt*
+- [F75](#f75) — mittel — *gefixt*
+- [F76](#f76) — mittel — *gefixt*
+- [F77](#f77) — mittel — *gefixt*
+- [F78](#f78) — mittel — *gefixt*
+- [F79](#f79) — niedrig — *gefixt*
+- [F80](#f80) — niedrig — *gefixt*
+- [F81](#f81) — niedrig — *gefixt*
+- [F82](#f82) — niedrig — *gefixt*
+- [F83](#f83) — niedrig — *gefixt*
+- [F84](#f84) — niedrig — *gefixt*
+- [F85](#f85) — mittel — *gefixt*
+- [F86](#f86) — mittel — *gefixt*
+- [F87](#f87) — niedrig — *gefixt*
+- [F88](#f88) — mittel — *gefixt*
+- [F89](#f89) — niedrig — *gefixt*
+- [F90](#f90) — mittel — *gefixt*
+- [F91](#f91) — niedrig — *gefixt*
+- [F92](#f92) — niedrig — *gefixt*
+- [F93](#f93) — niedrig — *gefixt*
+- [F94](#f94) — niedrig — *gefixt*
+- [F95](#f95) — mittel — *gefixt*
+- [F96](#f96) — niedrig — *gefixt*
+- [F97](#f97) — niedrig — *gefixt*
+- [F98](#f98) — niedrig — *gefixt*
+- [F99](#f99) — niedrig — *gefixt*
+- [F102](#f102) — niedrig — *gefixt*
+- [F103](#f103) — niedrig — *gefixt*
+- [F104](#f104) — niedrig — *gefixt*
+- [F105](#f105) — mittel — *gefixt*
+- [F106](#f106) — niedrig — *gefixt*
+- [F107](#f107) — niedrig — *gefixt*
+- [F108](#f108) — niedrig — *gefixt*
+- [F109](#f109) — niedrig — *gefixt*
+- [F111](#f111) — niedrig — *gefixt*
+- [F112](#f112) — mittel — *gefixt*
+- [F113](#f113) — niedrig — *gefixt*
+- [F114](#f114) — mittel — *gefixt*
+- [F115](#f115) — niedrig — *gefixt*
+- [F116](#f116) — niedrig — *gefixt*
+- [F117](#f117) — mittel — *gefixt*
+- [F118](#f118) — niedrig — *gefixt*
+- [F123](#f123) — niedrig — *gefixt*
+- [F125](#f125) — niedrig — *gefixt*
+- [F128](#f128) — mittel — *gefixt*
+- [F133](#f133) — niedrig — *gefixt*
+- [F134](#f134) — mittel — *gefixt*
+- [F137](#f137) — niedrig — *gefixt*
+- [F138](#f138) — niedrig — *gefixt*
+- [F143](#f143) — niedrig — *gefixt*
+- [F158](#f158) — niedrig bis mittel — *gefixt*
+- [F165](#f165) — mittel — *gefixt*
+- [F166](#f166) — niedrig — *gefixt (Java-API-Pfad; Server-Übersetzungen als F172)*
+- [F167](#f167) — niedrig bis mittel — *gefixt*
+- [F168](#f168) — niedrig — *gefixt*
+- [F172](#f172) — niedrig — *gefixt*
+- [F173](#f173) — niedrig — *gefixt*
+- [F174](#f174) — niedrig — *gefixt*
+- [F175](#f175) — niedrig — *gefixt*
+- [F176](#f176) — niedrig — *gefixt*
+- [F177](#f177) — niedrig — *gefixt*
+- [F178](#f178) — niedrig — *gefixt*
+- [F179](#f179) — niedrig — *gefixt*
+- [F180](#f180) — mittel — *gefixt (der Rest unter (14), Wahrscheinlichkeiten im gestreamten `/v1/completions`, mit F186)*
+- [F181](#f181) — niedrig bis mittel — *gefixt*
+- [F182](#f182) — mittel — *gefixt*
+- [F183](#f183) — niedrig — *gefixt*
+- [F184](#f184) — mittel — *gefixt*
+- [F185](#f185) — niedrig bis mittel — *gefixt*
+- [F186](#f186) — mittel — *gefixt*
+- [F187](#f187) — mittel — *gefixt*
+- [F188](#f188) — niedrig bis mittel — *gefixt*
+- [F189](#f189) — niedrig bis mittel — *gefixt*
+- [F190](#f190) — niedrig — *gefixt*
+- [F197](#f197) — mittel — *gefixt*
+- [F198](#f198) — mittel — *gefixt*
+- [F199](#f199) — mittel — *gefixt*
+- [F200](#f200) — niedrig bis mittel — *gefixt*
+- [F201](#f201) — niedrig — *gefixt*
+- [F202](#f202) — niedrig — *gefixt*
+- [F203](#f203) — niedrig — *gefixt*
+- [F204](#f204) — niedrig bis mittel — *gefixt*
+- [F205](#f205) — niedrig — *gefixt*
+- [F213](#f213) — niedrig — *gefixt*
+- [F214](#f214) — niedrig — *gefixt*
+- [F215](#f215) — niedrig — *gefixt*
+- [F216](#f216) — niedrig — *gefixt*
+- [F217](#f217) — niedrig — *gefixt*
+- [F218](#f218) — mittel — *gefixt*
+- [F219](#f219) — niedrig — *gefixt*
+- [F220](#f220) — niedrig — *gefixt*
+- [F221](#f221) — mittel — *gefixt*
+- [F222](#f222) — niedrig — *gefixt*
+- [F223](#f223) — niedrig — *gefixt*
+- [F224](#f224) — niedrig — *gefixt*
+- [F225](#f225) — niedrig — *gefixt*
+- [F226](#f226) — niedrig — *gefixt*
+- [F227](#f227) — niedrig — *gefixt*
+- [F228](#f228) — niedrig — *gefixt*
+- [F229](#f229) — niedrig — *gefixt*
+- [F230](#f230) — niedrig — *gefixt*
+- [F231](#f231) — niedrig — *gefixt*
+- [F232](#f232) — niedrig — *gefixt*
+- [F233](#f233) — niedrig — *gefixt*
+- [F234](#f234) — niedrig — *gefixt*
+- [F235](#f235) — niedrig — *gefixt*
+- [F236](#f236) — niedrig — *gefixt*
+- [F237](#f237) — niedrig — *gefixt*
+- [F238](#f238) — niedrig — *gefixt*
+- [F239](#f239) — niedrig — *gefixt*
+- [F240](#f240) — niedrig — *gefixt*
+- [F241](#f241) — niedrig bis mittel — *gefixt*
+- [F242](#f242) — niedrig — *gefixt*
+- [F243](#f243) — niedrig — *gefixt*
+- [F244](#f244) — niedrig — *gefixt*
+- [F245](#f245) — niedrig — *gefixt*
+- [F246](#f246) — mittel — *gefixt*
+- [F247](#f247) — mittel — *gefixt*
+- [F248](#f248) — mittel — *gefixt*
+- [F249](#f249) — niedrig bis mittel — *gefixt*
+- [F250](#f250) — niedrig — *gefixt*
+- [F251](#f251) — niedrig bis mittel — *gefixt*
+- [F252](#f252) — niedrig bis mittel — *gefixt*
+- [F253](#f253) — niedrig bis mittel — *gefixt*
+- [F254](#f254) — niedrig — *gefixt*
+- [F255](#f255) — niedrig — *gefixt*
+- [F256](#f256) — niedrig — *gefixt*
+- [F257](#f257) — niedrig bis mittel — *gefixt*
+- [F258](#f258) — mittel — *gefixt*
+- [F259](#f259) — niedrig bis mittel — *gefixt*
+- [F260](#f260) — niedrig — *gefixt*
+- [F261](#f261) — niedrig bis mittel — *gefixt*
+- [F262](#f262) — niedrig — *gefixt*
+- [F263](#f263) — niedrig — *gefixt*
+- [F264](#f264) — niedrig — *gefixt*
+- [F265](#f265) — niedrig — *gefixt*
+- [F266](#f266) — mittel — *gefixt*
+- [F267](#f267) — niedrig — *gefixt*
+- [F269](#f269) — mittel — *gefixt*
+- [F270](#f270) — niedrig (Testfehler) — *gefixt*
+- [F271](#f271) — niedrig bis mittel — *gefixt*
+- [F272](#f272) — niedrig (Testfehler) — *gefixt*
+
+## Funde
+
+
+### F01 – java-llama.cpp: `TrainingParameters` validiert die dokumentierten Grenzen nicht
+- **Zeit:** 2026-10-16 22:50 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/TrainingParameters.java:46` (`epochs`, „at least 1“), `:19` (Klassen-Javadoc „exactly one of trainingText / trainingFile“), `valSplit`
+- **Schwere:** niedrig
+- **Beschreibung:** Javadoc (und `LlamaTrainer.finetune(Path,String,Path,int,float)`) sagt `epochs` „at least 1“ und „genau eins von trainingText/trainingFile“. Nichts davon wird geprüft: `epochs(0)` oder negativ läuft nativ 0 Epochen und `train_engine.cpp` schreibt danach das **unveränderte** Modell nach `outputPath` – ein stiller Erfolg ohne Training. Sind beide Korpus-Felder gesetzt, gewinnt nativ stillschweigend `trainingFile`. `valSplit` außerhalb `[0,1)` ergibt in `train_engine.cpp:75` einen negativen bzw. 0-großen `idata_split` (Verhalten in ggml nicht geprüft, möglicherweise ein `GGML_ASSERT`, der die JVM beendet). Fehlende `modelPath`/`outputPath` geben eine nackte NPE aus `toJson()`.
+- **Reproduktion:** `TrainingParametersTest#lessThanOneEpochIsRejected`, `#validationSplitOutsideZeroToOneIsRejected`, `#bothOrNoCorpusIsRejected`, `#missingPathsNameTheField`
+- **Fix:** `toJson()` ruft `validate()`: `modelPath`/`outputPath` per `Objects.requireNonNull` mit Feldnamen, genau eins von `trainingText`/`trainingFile`, `epochs >= 1`, `valSplit` in `[0, 1)` (NaN eingeschlossen) – sonst `IllegalArgumentException` vor dem JNI-Aufruf.
+- **Rot-Grün-Nachweis:** ohne Fix rot, 4 von 7 („Expected IllegalArgumentException to be thrown, but nothing was thrown“ bzw. bare NPE ohne Feldnamen); mit Fix grün 7/7 (skipped 0), `WireNameRegistryTest` 8/8; Gegenprobe per `git stash` rot 4/7, nach `stash pop` grün 7/7. `WireNameRegistryTest#everyTrainingFieldIsWrittenByAFullyPopulatedConfiguration` setzte beide Korpus-Felder zugleich und nutzt jetzt zwei Konfigurationen.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `aaa7b5a`
+- **Doku-Lücke aus diesem Fix (2026-10-17, nur beschrieben):** Seit dem Fix wirft `TrainingParameters.toJson()` – und damit beide `LlamaTrainer.finetune`-Überladungen – bei ungültiger Konfiguration `IllegalArgumentException`; die Javadoc beider `finetune` nennt nur `LlamaException`. Reine Doku, kein Test kann sie rot machen, daher nach der Nutzerregel unverändert.
+- **Nachtrag (Branch-Review 2026-10-20, Fund J8-5):** Die Doku-Lücke ist geschlossen. `TrainingParameters#toJson` nennt `@throws IllegalArgumentException` (weniger als eine Epoche, `valSplit` außerhalb `[0, 1)`, nicht genau ein Korpus) und `@throws NullPointerException` (`modelPath`/`outputPath` am Builder nicht gesetzt; die Meldung nennt das Feld -- die NPE gab es schon vor dem Fix, er gab ihr nur den Namen). `LlamaTrainer#finetune(TrainingParameters)` nennt beide mit Verweis auf `toJson`; die Bequemlichkeits-Überladung nur `IllegalArgumentException` für `epochs` unter 1, die einzige dort erreichbare Prüfung (Text als Parameter, keine Datei, `valSplit` auf dem Standard, Pfade nicht null unter `@NullMarked`). Nur Doku; die Prüfungen pinnt `TrainingParametersTest`.
+
+
+### F05 – java-llama.cpp: `ChatRequest.buildToolsJson()` kürzt ein Tool-Schema still auf den ersten JSON-Wert
+- **Zeit:** 2026-10-16 23:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/ChatRequest.java` (`buildToolsJson`, Zeile mit `SCHEMA_READER.readTree`)
+- **Schwere:** niedrig
+- **Beschreibung:** Das Schema jeder `ToolDefinition` wurde mit einfachem `readTree` gelesen; das liest den ersten JSON-Wert und ignoriert den Rest. `{"type":"object"} {"required":["x"]}` wurde so ohne Fehler als `{"type":"object"}` gesendet – ein anderes Schema als vom Aufrufer geschrieben. `JsonParameters` nutzt aus genau diesem Grund `FAIL_ON_TRAILING_TOKENS` (siehe CLAUDE.md), `ChatRequest` umging das. Jetzt gleicher strikter Reader → solche Strings nehmen den vorhandenen Fallback für ungültiges JSON.
+- **Weitere Beobachtung (nicht geändert):** Ungültiges JSON wird absichtlich als **String** unter `parameters` weitergereicht, `""` und Leerzeichen werden zu `"parameters":null`. Ob eine `IllegalArgumentException` (wie in `JsonParameters`) besser wäre, ist eine API-Entscheidung.
+- **Reproduktion:** `ChatRequestTest$…#buildToolsJsonDoesNotSilentlyTruncateASchemaWithTrailingContent` (vorher rot, danach grün), plus Positivtest `buildToolsJsonEmbedsAValidSchemaAsAnObject`; `ChatRequestTest` 24/24 grün. `spotbugs:check` nicht gelaufen.
+- **Nachtrag (Branch-Review 2026-10-20):** Die „API-Entscheidung“ ist mit F233 gefallen: `appendTool` weist ein Schema, das nicht genau ein JSON-Wert ist, mit `IllegalArgumentException` samt Werkzeugnamen ab; der String-Rückfall ist weg, der Test oben durch `appendToolRejectsASchemaThatIsNotExactlyOneJsonValue` ersetzt.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `d6a9be5`
+
+
+### F06 – java-llama.cpp: `RouterClient.awaitModelLoaded(id, Long.MAX_VALUE)` bricht sofort mit Timeout ab
+- **Zeit:** 2026-10-16 23:35 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/RouterClient.java` (`awaitModelLoaded`, Deadline-Berechnung)
+- **Schwere:** mittel
+- **Beschreibung:** `System.currentTimeMillis() + timeoutMillis` läuft bei `Long.MAX_VALUE` („so lange wie nötig“, eine naheliegende Aufrufform) über und wird negativ. Die Schleife pollt nie und wirft „did not reach LOADED within 9223372036854775807 ms (last status: null)“, auch wenn das Modell längst geladen ist. Fix: Deadline sättigt bei `Long.MAX_VALUE`.
+- **Reproduktion:** `RouterClientTest#awaitModelLoaded_longMaxValueTimeoutStillPolls` (vorher rot mit genau dieser Meldung, danach grün)
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `a4e5b22`
+
+
+### F07 – java-llama.cpp: `RouterClient` mit IPv6-Literal (`"::1"`) kann nie verbinden
+- **Zeit:** 2026-10-16 23:40 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/RouterClient.java` (`request`, URL-Aufbau)
+- **Schwere:** niedrig
+- **Beschreibung:** Die URL wurde als `"http://" + host + ":" + port` gebaut. Für `new RouterClient("::1", port)` (Router mit `--host ::1`; Javadoc: „host name or address“) entsteht `http://::1:port/models` → `MalformedURLException: Error at index 0 in ":1:8080"` vor jedem Verbindungsversuch. Workaround war `"[::1]"`. Fix: IPv6-Literal wird geklammert, bereits geklammerte Hosts bleiben unverändert. Nicht abgedeckt: Zonen-IDs (`fe80::1%eth0`) bräuchten zusätzlich `%25`-Kodierung. `toString()` zeigt den Host weiterhin ungeklammert.
+- **Reproduktion:** `RouterClientTest#ipv6LiteralHostIsBracketedInTheRequestUrl` (vorher rot: `MalformedURLException`, danach andere `IOException`, weil auf Port 1 nichts lauscht). Der Sandbox fehlt IPv6, ein echter Server auf `::1` war hier nicht testbar.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `5b98dc7`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J7-3):** `toString()` zeigt den Host jetzt wie die Anfrage-URL (`urlHost`): `RouterClient("::1", 8080)` wird zu `RouterClient(http://[::1]:8080)` statt der ungültigen Autorität `http://::1:8080`; die Klassen-Javadoc („renders as its target URL“) stimmt damit. Test `RouterClientTest#toStringBracketsAnIpv6HostAsTheRequestUrlDoes` (auch `"[::1]"` bleibt einfach geklammert); ohne die Änderung rot („Expected: is "RouterClient(http://[::1]:8080)" but: was "RouterClient(http://::1:8080)"“), mit ihr grün (`RouterClientTest` 22/22).
+- **Nachtrag (Branch-Review 2026-10-20, Fund J7-1):** „bereits geklammerte Hosts bleiben unverändert“ hat jetzt einen Test über den Anfrageweg: `RouterClientTest#alreadyBracketedIpv6HostIsNotBracketedAgain` (`new RouterClient("[::1]", 1)`, `listModels()` wirft eine `IOException`, keine `MalformedURLException`). Ohne `!host.startsWith("[")` entsteht `http://[[::1]]:1/models`, das `URI.create` als `IllegalArgumentException` ablehnt – außerhalb des `IOException`-Vertrags von `request()`. Gegenprobe rot („Unexpected exception type thrown, expected:  but was: “); am HEAD fängt dieselbe Mutation auch der `toString()`-Test aus J7-3, weil beide `urlHost` teilen; der neue Test pinnt den Vertrag der Anfrage selbst. `RouterClientTest` 23/23 grün.
+
+
+### F09 – java-llama.cpp: Loader löscht fremde `llama*`/`ggml*`-Einträge im Temp-Verzeichnis, Verzeichnisse rekursiv
+- **Zeit:** 2026-10-16 23:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/loader/LlamaLoader.java` (`isStaleExtraction` / `cleanup` / `cleanPath`)
+- **Schwere:** hoch (Datenverlust außerhalb der eigenen Dateien)
+- **Beschreibung:** `cleanup()` lief bei jedem Start, der die Natives-Jars erreicht, über `java.io.tmpdir` (bzw. `net.ladenthin.llama.tmpdir`) und löschte **sofort und rekursiv** jeden Eintrag, dessen Name mit `jllama`, `llama` oder `ggml` beginnt und kein `jllama-backend-*`-Verzeichnis ist. Gedacht war das für die einzelnen Bibliotheksdateien des flachen Layouts vor 6.0.0 (so steht es in der Javadoc). Getroffen wurde aber auch z. B. ein `/tmp/llama.cpp`-Checkout, ein `/tmp/ggml-models/`-Ordner oder ein heruntergeladenes `/tmp/llama-2-7b.Q4_K_M.gguf`. Fix: Aus dem alten Layout werden nur noch **reguläre Dateien mit Bibliotheksendung** gelöscht (`.so`, `.dll`, `.dylib`, `.jnilib`, `.metal`).
+- **Reproduktion:** `LlamaLoaderTest#staleExtractionLeavesForeignEntriesWithAMatchingPrefixAlone` (vorher rot, danach grün); `LlamaLoaderTest` 63/63, inkl. der bestehenden Fälle `jllama-3f2a.so` / `ggml-metal.metal` (werden weiter gelöscht).
+- **Bitte prüfen:** Die genauen Dateinamen des alten Layouts konnte ich nicht verifizieren, weil der Clone flach ist und keine Historie vor 6.0.0 enthält. Falls es damals auch Endungen wie `.tmp` oder `.lck` gab, bleiben die jetzt liegen. Das ist harmlos, nur Restmüll.
+- **Nachtrag (Branch-Review 2026-10-20):** Am Tag `v5.1.0` geprüft: das alte Layout entpackte `System.mapLibraryName("jllama")` flach (`libjllama.so`, `libjllama.dylib`, `jllama.dll`), unter macOS dazu `ggml-metal.metal`, jede Kopie über `Files.createTempFile(dir, name + ".", ".tmp")`. `libjllama.so`/`libjllama.dylib` trugen nie ein Bereinigungspräfix und wurden von keiner Version entfernt; was dieser Fix entfernt, sind `jllama.dll` (und ältere `llama*`/`ggml*`-DLLs) und `ggml-metal.metal`. Die `..tmp`-Reste einer abgebrochenen Kopie blieben seit diesem Fix liegen; seit F239 gehen sie wieder, sobald sie `STALE_EXTRACTION_AGE` alt sind. Das „Bitte prüfen“ ist damit erledigt.
+- **Nachtrag (Branch-Review 2026-10-20):** Die Endungsliste und der Reguläre-Datei-Wächter waren nur zum Teil getestet: `.dll`, `.dylib` und `.jnilib` ließen sich aus `FLAT_LAYOUT_SUFFIXES` streichen und `NOFOLLOW_LINKS` weglassen, ohne dass etwas rot wurde (Fund J10-3). Neu in `LlamaLoaderTest`: `#flatLayoutLibraryNamesAreTheLibrarySuffixes` (alle fünf Endungen ja, `.gguf`/`.log`/`.tmp`/`llama.cpp` nein), `#staleExtractionKeepsADirectoryNamedLikeALibrary` (ein Verzeichnis `jllama.dll` mit Inhalt bleibt) und `#staleExtractionKeepsASymlinkNamedLikeALibrary` (ein Link `ggml.dll` auf eine fremde Datei bleibt; übersprungen, wo keine Links angelegt werden dürfen). Gegenprobe rot: ohne den Wächter die beiden neuen Fälle und der Verzeichnisfall des F239-Tests (`expected:  but was: `; seit F239 steht der Wächter vor beiden Zweigen, deckte also den Zweig ohne `.tmp` schon mit), mit einem link-folgenden `isRegularFile(path)` der Link-Test, ohne `.dll`/`.dylib`/`.jnilib` je der Endungstest (`jllama.dll ==> expected:  but was: ` usw.); unverändert 67/67 grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `61ea8f5`
+
+
+### F12 – java-llama.cpp: Ollama-`/api/chat` verwirft Bilder (`images`), obwohl `/api/show` „vision“ meldet
+- **Zeit:** 2026-10-16 23:55 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java` (`toOpenAiMessage`)
+- **Schwere:** mittel (Feature stillschweigend wirkungslos)
+- **Beschreibung:** Ollama-Clients schicken Bilder als `"images": ["", …]` neben `"content"`. `toOpenAiMessage` kopierte die Nachricht unverändert; der OpenAI-Parser von llama.cpp liest nur `image_url`-Content-Parts, das Feld `images` wurde also ignoriert. Das Vision-Modell antwortete ohne Bild, kein Fehler. Gleichzeitig meldet `/api/show` die Fähigkeit `vision`, wenn `--mmproj` gesetzt ist. Fix: `images` wird entfernt und als `image_url`-Parts (Data-URL, MIME-Typ aus den Magic-Bytes: JPEG/GIF/WebP, sonst PNG – llama.cpp dekodiert nach Inhalt) hinter dem Text angehängt.
+- **Reproduktion:** `OllamaApiSupportTest#toOpenAiChatRequestTurnsOllamaImagesIntoImageUrlParts` (vorher rot, danach grün); `OllamaApiSupportTest` 20/20, `OpenAiCompatServerHttpTest` 43/43. Ende-zu-Ende mit echtem Vision-Modell nicht geprüft (kein Modell im Sandbox).
+- **Nebenbefund (nicht geändert):** Die gestreamte Abschlusszeile (`toOllamaDoneLine`) setzt `done_reason` immer auf `"stop"`, auch bei Abbruch durch `length`; die nicht-gestreamte Antwort übernimmt `finish_reason` korrekt.
+- **Nachtrag (Branch-Review 2026-10-20):** Die MIME-Erkennung aus den Magic-Bytes und der Durchlass einer fertigen `data:`-URL hatten keinen Test; der PNG-Test oben nimmt nur den `else`-Zweig (Fund J3-6). `OllamaApiSupportTest#ollamaImageMimeTypeFollowsTheMagicBytes` pinnt `/9j/` → `image/jpeg`, `R0lGOD` → `image/gif`, `UklGR` → `image/webp` und den PNG-Rückfall; `#ollamaImageThatIsAlreadyADataUrlIsPassedThrough` den Durchlass — der einzige Zweig mit sichtbarer Wirkung, denn ein doppeltes `data:` lehnt llama.cpp ab, während es den Untertyp nie liest. Gegenprobe rot: vertauschte JPEG/GIF-Literale (`Expected: is "data:image/jpeg;base64,/9j/4AAQ" but: was "data:image/gif;base64,/9j/4AAQ"`), fehlender WebP-Zweig (`… but: was "data:image/png;base64,UklGRh4A"`), fehlender Durchlass (`… but: was "data:image/png;base64,data:image/jpeg;base64,/9j/4AAQ"`); unverändert 46/46 in der Klasse grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `ef7295c`
+
+
+### F13 – java-llama.cpp: `setRopeScaling(UNSPECIFIED | LONGROPE | MAX_VALUE)` macht das Modell unladbar
+- **Zeit:** 2026-10-17 00:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/ModelParameters.java` (`setRopeScaling`), `args/RopeScalingType.java`
+- **Schwere:** mittel
+- **Beschreibung:** llama.cpp akzeptiert bei `--rope-scaling` nur `none`, `linear`, `yarn` (`common/arg.cpp` @ b11639, Zeile 2337ff.) und wirft sonst `invalid value` → `LlamaModel` lädt nicht („Failed to parse model parameters“). `RopeScalingType` bietet zusätzlich `UNSPECIFIED` (laut Javadoc „use the model default“), `LONGROPE` und `MAX_VALUE`; der Setter reichte sie ungeprüft als `unspecified`/`longrope`/`maxvalue` durch. Genau die Fehlerklasse aus CLAUDE.md („a Java test asserting the string mapping passes forever“): `testSetRopeScalingAllValues` prüfte nur die String-Abbildung aller Konstanten. Gegencheck aller anderen `args`-Enums gegen den Upstream-Parser (CacheType, DraftSampling, FlashAttn, GpuSplitMode, LazyMode, LoadMode, MiroStat, NumaStrategy, PoolingType, ReasoningFormat, Sampler): alle Werte akzeptiert.
+- **Fix:** `UNSPECIFIED` entfernt die Option (wie `setPoolingType`), `LONGROPE`/`MAX_VALUE` → `IllegalArgumentException` beim Aufruf. Kein vorher funktionierender Pfad geht verloren (diese drei Werte scheiterten ohnehin beim Laden). Die Enum-Konstanten selbst bleiben (API-Kompatibilität) – ob sie gelöscht werden sollen, ist deine Entscheidung (CLAUDE.md: „A name with no counterpart is deleted, never deprecated“).
+- **Reproduktion:** `ModelParametersExtendedTest#testSetRopeScalingUnspecifiedLeavesTheModelDefault`, `#testSetRopeScalingRejectsValuesTheParserDoesNotAccept` (beide vorher rot), `#testSetRopeScalingAllValuesTheParserAccepts` (ersetzt den alten Test). `WireNameRegistryTest`-Treiber nimmt bei Ablehnung die nächste Konstante. Grün: ModelParametersExtendedTest 148, ModelParametersTest 92, WireNameRegistryTest 8, RopeScalingTypeTest 24. Nicht gelaufen: C++-`test_model_flags` (prüft nur Optionsnamen, nicht Werte), `spotbugs:check`.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `abe776c`
+
+
+### F16 – java-llama.cpp: `close()` parallel zu `synthesize()` gibt die TTS-Engine frei, während sie läuft (Use-after-free → JVM-Absturz)
+- **Zeit:** 2026-10-17 00:50 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/TextToSpeech.java:95-113` (JNI) und `llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmTextToSpeech.java` (FFM); nativ `llama/src/main/cpp/tts_engine.cpp:201` (`engine_free` = `delete engine`), `jllama_api.cpp:673`
+- **Schwere:** mittel (Fehlbedienung durch zwei Threads, aber Folge ist ein nativer Absturz statt einer Exception)
+- **Beschreibung:** `synthesize()` prüft nur `handle == 0` bzw. `closed.get()` und ruft dann nativ auf, ohne Lock. `close()` (JNI: `synchronized`, FFM: CAS) ruft `jllama_tts_free` → `delete engine`. Schließt Thread B, während Thread A in `engine_synthesize` steckt, löscht B die Engine samt `synthesize_mutex`, `llama_context` und `mtmd`-Kontext unter A weg → Use-after-free im nativen Code. Bei `LlamaModel` gibt es dafür die Nutzer-Referenzzählung (`jllama_context_guard`, `free` wartet auf `users == 0`); die TTS-Engine hat nichts Vergleichbares. Mehrere gleichzeitige `synthesize()` sind dagegen sicher (nativ per `synthesize_mutex` serialisiert).
+- **Vorschlag:** In beiden Klassen `synthesize` und `close` über denselben Monitor laufen lassen (z. B. `synchronized` auch auf `synthesize`; kostet nichts, weil nativ ohnehin serialisiert wird) – oder eine Referenzzählung wie bei `jllama_context` in `jllama_api.cpp`.
+- **Reproduktion:** `TextToSpeechLifecycleTest#close_whileSynthesizeIsInsideTheEngine_freesItOnlyAfterSynthesizeReturned` (Fake über die neue paketinterne Naht `TextToSpeech.Backend` hält `synthesize` im „nativen“ Aufruf fest), dazu `#synthesize_afterClose_throwsIllegalState`, `#close_calledTwice_freesTheEngineOnce`, `#synthesize_whileOpen_returnsTheBackendsWav`
+- **Fix:** `ReentrantReadWriteLock`: `synthesize` hält die Lesesperre über den ganzen nativen Aufruf, `close` die Schreibsperre. Gleichzeitige Synthesen bleiben möglich (die Engine ordnet sie selbst über `synthesize_mutex`), aber `close` gibt die Engine (samt diesem Mutex) erst frei, wenn keine mehr läuft. Die JNI-Aufrufe laufen über eine paketinterne Naht `Backend`; `LlamaLoader.initialize()` wanderte aus dem static-Block in deren `load`.
+- **Rot-Grün-Nachweis:** Naht zuerst verhaltensneutral eingezogen; dagegen rot 1/4 („the engine was freed while synthesize() still ran in it – Expected: is  but: was “). Mit Fix grün 4/4 (skipped 0); Gegenprobe mit dem Stand ohne Fix rot 1/4, wiederhergestellt grün 4/4. `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt (JNI; die FFM-Hälfte als F198)
+- **Commit:** java-llama.cpp `6ed949e`
+
+
+### F17 – java-llama.cpp: Responses-API (`POST /v1/responses`) verwirft `input_image`-Teile → Vision-Modell sieht kein Bild
+- **Zeit:** 00:10 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:138` (`appendInput`, Fall `message`)
+- **Schwere:** mittel
+- **Beschreibung:** Der Translator flachte jedes Content-Array einer `message` auf seinen Text ab (`inputContentText` liest nur `text`). Ein `{"type":"input_image","image_url":"data:…"}`-Teil kam nie im Chat-Kern an; ein Vision-Modell antwortete, als wäre kein Bild geschickt worden – still, ohne Fehler. Gleiche Fehlerklasse wie der Ollama-`images`-Fund oben.
+- **Fix:** Enthält das Array `input_image`-Teile, wird ein OpenAI-Content-Part-Array gebaut (`text`-Teile + `image_url`-Teile; `image_url` als String oder `{url}`). Reine Text-Nachrichten bleiben unverändert ein String. `file_id`-Bilder (ohne URL) werden weiterhin ignoriert.
+- **Reproduktion:** `ResponsesApiSupportTest#requestTurnsInputImagePartsIntoImageUrlParts` (vorher rot, nachher 10/10 grün)
+- **Nachtrag (Branch-Review 2026-10-20):** Der Satz „`file_id`-Bilder (ohne URL) werden weiterhin ignoriert“ gilt nicht mehr: ein `input_image` ohne URL wird seit F224 wie upstream mit 400 abgewiesen.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `558422a`
+
+
+### F18 – java-llama.cpp: `NativeServer.close()` nicht atomar → doppeltes Freigeben des nativen Servers bei parallelen Aufrufen
+- **Zeit:** 00:17 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/NativeServer.java:284` (`close`, ebenso `isRunning`, `start`)
+- **Schwere:** mittel
+- **Beschreibung:** `close()` las `handle`, prüfte auf 0 und setzte es danach auf 0 – ohne Synchronisation. Zwei gleichzeitige `close()` (Shutdown-Hook + try-with-resources, oder in `NativeServer.main` der Hook und der `finally`-Block, wenn der Worker genau beim Ctrl-C selbst endet) kamen beide durch und riefen `stopNativeServer(h)` doppelt: zweimal `join()` und zweimal `delete` desselben `native_server` → JVM-Absturz. Ebenso konnte `isRunning()` einen Handle abfragen, den ein paralleles `close()` gerade freigab (Use-after-free). Der Kommentar in `main` („the finally and the hook both firing is safe“) gilt nur nacheinander.
+- **Fix:** privates `lifecycleLock` serialisiert `start`, `isRunning` und `close` (keine `synchronized`-Methoden, SpotBugs USO_UNSAFE_METHOD_SYNCHRONIZATION). `spotbugs:check` grün, `NativeServerSmokeTest` + `LlamaArchitectureTest` grün.
+- **Reproduktion:** `NativeServerLifecycleTest` über die neue paketinterne Naht `NativeServer.Backend` (ein Fake statt der JNI-Aufrufe, damit kein erfundener Handle libjllama erreicht): `close_whileIsRunningIsInsideTheNativeCall_stopsOnlyAfterItReturned`, `close_whileStartIsStillStarting_stopsTheServerOnceItStarted`, `close_calledTwice_stopsTheHandleOnce`. Der Fake hält `isRunning` bzw. `start` im „nativen“ Aufruf fest, dadurch sind beide Race-Fenster deterministisch. (Die alte Probe `/home/user/BUGHUNT-2026-10-17-F18-probe.java`: vorher 4 / 20 000 doppelte Stopps, nachher 0 / 20 000.)
+- **Rot-Grün-Nachweis:** Gegen den Lebenszyklus vor `5df523f` (ohne `lifecycleLock`, Naht beibehalten) **rot**, 2 von 3: „the handle was stopped while isRunning() still used it – Expected: is  but: was “ und „close() returned while the server was still starting, so it kept running – Expected: is <1> but: was <0>“. Mit Fix **grün** 3/3 (skipped 0), dreimal hintereinander, ebenso `NativeServerSmokeTest` 10/10, `NativeServerWorkerCommandTest` 5/5, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün. Das doppelte `close()` aus zwei Threads selbst ist mit der Naht nicht steuerbar (das Fenster liegt zwischen Lesen und Nullsetzen des Feldes); `close_calledTwice_stopsTheHandleOnce` deckt nur den sequentiellen Fall ab.
+- **Status:** gefixt (Fix `5df523f`; roter Regressionstest nachgereicht, bis dahin „nur beschrieben“, siehe `d5c2622`)
+- **Commit:** java-llama.cpp `5df523f` (Fix), Test und Naht `8845eca`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J7-2):** Die drei Wächter von `start()` (älter als dieser Fix, erst über die Naht ohne Modell testbar) haben jetzt Tests in `NativeServerLifecycleTest`: `#start_whenTheBackendThrowsALlamaException_freesTheSingleInstanceSlot` und `#start_whenTheBackendThrowsAnError_freesTheSingleInstanceSlot` (beide Arme von `catch (RuntimeException | Error)`, `UnsatisfiedLinkError` wie aus `LlamaLoader.initialize()`), `#start_calledTwiceOnTheSameInstance_throwsIllegalStateException` („already started“, der Fake einmal gestartet, danach genau ein `stop`) und `#start_whileAnotherServerRuns_throwsIllegalStateException` („another NativeServer“, der zweite Fake nie gestartet, ein dritter ebenso abgewiesen, der Platz also weiter belegt). `FakeBackend` bekam dafür `starts` und `startFailure`. Gegenprobe rot: ohne `RUNNING.set(false)` im `catch` („Unexpected exception type thrown, expected:  but was: “, die folgenden Tests der Klasse kaskadieren, weil der Platz prozessweit belegt bleibt), ohne die Handle-Prüfung (`containsString("already started")` scheitert an der Meldung des anderen Wächters), ohne den CAS („Expected java.lang.IllegalStateException to be thrown, but nothing was thrown.“); unverändert 7/7 grün.
+- **Nachtrag (Branch-Review 2026-10-20, Fund J7-4):** Der Kommentar in `main` nannte noch die Null-Prüfung des Handles als Grund, warum `finally` und Shutdown-Hook beide `close()` rufen dürfen. Er sagt jetzt, dass `close()` idempotent **und** über `lifecycleLock` serialisiert ist, beide also gleichzeitig feuern dürfen (Shutdown-Hooks laufen in eigenem Thread) und der native Server genau einmal gestoppt wird. Reiner Kommentar, kein Test.
+
+
+### F24 – java-llama.cpp: `OpenAiServerCli` bewirbt bei kleinem `-c` fast kein Eingabebudget (`-c 2048` → 1 Token)
+- **Zeit:** 05:55 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiServerCli.java:721` (`Options.toServerConfig`)
+- **Schwere:** mittel
+- **Beschreibung:** Das Ausgabebudget ist `min(2048, ctx/2)`, das Eingabebudget wurde aber als `ctx − 2048` (der Default, nicht das tatsächliche Ausgabebudget) berechnet. Für `ctx < 4096` passt das nicht zusammen: `-c 2048` → Eingabe **1** Token, Ausgabe 1024; `-c 1024` → beworbenes Fenster 513 statt 1024; `-c 3000` → 2452. Das Eingabebudget landet in `maxInputTokens` (Copilot-Konfigurationsausgabe) und in der beworbenen Kontextlänge (`/api/show`, `/props`: `maxInputTokens + maxOutputTokens`); Clients, die ihren Prompt danach zuschneiden, schicken praktisch nichts. Für `-c ≥ 4096` (README-Beispiel 8192 → 6144) war das Ergebnis zufällig richtig.
+- **Fix:** `maxInputTokens = max(1, ctx − maxOutputTokens)` mit dem tatsächlich gesetzten Ausgabebudget; 8192 → 2048/6144 unverändert.
+- **Reproduktion:** `OpenAiServerCliTest#smallContextSizeSplitsTheWholeWindowBetweenInputAndOutput` (ctx 1024/2048/3000; vorher `Expected 1024 but was 513`, nachher 38/38 grün).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `2cf3640`
+
+
+### F25 – java-llama.cpp: `OpenAiCompatServer` mit `--api-key` lehnt jeden Anthropic-Client ab (`x-api-key`) und `bearer` in Kleinschreibung
+- **Zeit:** 06:03 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java:893` (`authorized`; gefunden beim Review von `OpenAiServerConfig`)
+- **Schwere:** mittel
+- **Beschreibung:** Akzeptiert wurde nur `Authorization: Bearer ` mit exakt großgeschriebenem `Bearer`. Die Anthropic-Messages-Oberfläche (`POST /v1/messages`) wird von Anthropic-SDKs aber per `x-api-key`-Header authentifiziert – mit gesetztem `--api-key` bekam jeder solche Client 401, die Anthropic-Oberfläche war damit nur ohne Schlüssel nutzbar. Zudem ist der Schema-Name laut RFC 7235 case-insensitiv; `bearer ` wurde abgelehnt.
+- **Fix:** `x-api-key` wird zusätzlich akzeptiert, das Schema case-insensitiv geprüft; beide Vergleiche weiterhin zeitkonstant (`MessageDigest.isEqual`). `spotbugs:check` grün.
+- **Reproduktion:** `OpenAiCompatServerHttpTest#anthropicApiKeyHeaderAndLowercaseBearerSchemeAreAccepted` (vorher 401 statt 200, nachher Klasse 44/44 grün).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `78fd0af`
+
+
+### F26 – java-llama.cpp: `withTokenIdBias`/`withTokenBias` mit `Float.NEGATIVE_INFINITY` sperrt das Token nicht – wird still ignoriert
+- **Zeit:** 06:09 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/ParameterJsonSerializer.java:236` (`buildTokenIdBiasArray`/`buildTokenStringBiasArray`; gefunden beim Review von `InferenceParameters`)
+- **Schwere:** mittel
+- **Beschreibung:** JSON kennt kein Unendlich; Jackson schreibt einen nicht-endlichen `float` als String: `{"logit_bias":[[5,"-Infinity"]]}` (gemessen). llama.cpps Parser (`server-schema.cpp` b11639, `parse_bias`) akzeptiert nur eine Zahl oder `false` und überspringt alles andere per `continue`. Der naheliegende Weg, ein Token zu verbieten (`withTokenIdBias(Map.of(id, Float.NEGATIVE_INFINITY))`), bewirkte damit gar nichts – ohne Fehler. `NaN` und `+Infinity` ebenso.
+- **Fix:** `-Infinity` wird als `false` (llama.cpps Schreibweise für ein Verbot, wie schon bei `withDisabledTokenIds`) gesendet, `NaN`/`+Infinity` werfen `IllegalArgumentException`.
+- **Reproduktion:** `ParameterJsonSerializerTest#testBuildTokenIdBiasArray_negativeInfinityBansTheToken`, `#testBuildTokenIdBiasArray_nanOrPositiveInfinityIsRejected` (vorher rot, nachher 34/34); `InferenceParametersTest` 105/105, `WireNameRegistryTest` 8/8 grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `d38840e`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J8-6):** Die öffentliche Javadoc nannte das Verhalten nicht, nur die private `addBias`. `ParameterJsonSerializer#buildTokenIdBiasArray`/`#buildTokenStringBiasArray` und `InferenceParameters#withTokenIdBias`/`#withTokenBias` sagen jetzt, dass `Float.NEGATIVE_INFINITY` als llamas Sperrform `false` geht, und nennen `@throws IllegalArgumentException` für NaN und +Unendlich. Nur Doku; `ParameterJsonSerializerTest` pinnt beides.
+
+
+### F28 – java-llama.cpp: `InferenceParameters.withMessages(List)` verwirft `tool_calls` und `tool_call_id`
+- **Zeit:** 06:22 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/ParameterJsonSerializer.java:152` (`buildMessages(List)`)
+- **Schwere:** mittel
+- **Beschreibung:** Der gemeinsame Serializer schrieb nur `role` und `content` (bzw. Content-Parts). `ChatRequest.buildMessagesJson` ergänzte `tool_calls`/`tool_call_id` nachträglich selbst – `InferenceParameters.withMessages(List)` nutzt den Serializer aber direkt. Eine Tool-Calling-Runde über diesen Weg (`ChatMessage.assistantToolCalls(...)`, `ChatMessage.toolResult(...)`) schickte einen Assistant-Turn ohne seine Aufrufe und ein Tool-Ergebnis ohne Zuordnung: `[{"role":"assistant","content":""},{"role":"tool","content":"42"}]` (gemessen).
+- **Fix:** die beiden Felder schreibt jetzt der Serializer; die Duplikat-Schleife in `ChatRequest` entfällt (Ausgabe dort identisch). `spotbugs:check` grün.
+- **Reproduktion:** `InferenceParametersTest#withChatMessagesKeepsToolCallsAndToolCallIds` (vorher rot, nachher 106/106); `ChatRequestTest` 24/24, `ParameterJsonSerializerTest` 34/34, `OpenAiCompatServerHttpTest` 44/44 grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `2c32c84`
+
+
+### F29 – java-llama.cpp: Anthropic-Messages-API (`POST /v1/messages`) verwirft `image`-Blöcke → Vision-Modell sieht kein Bild
+- **Zeit:** 06:29 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java:151` (`appendOpenAiMessages`)
+- **Schwere:** mittel
+- **Beschreibung:** Gleiche Fehlerklasse wie F17 (Responses) und der Ollama-`images`-Fund: der Translator kannte nur `text`, `tool_use` und `tool_result`; ein `{"type":"image","source":{"type":"base64","media_type":…,"data":…}}`-Block (so senden Anthropic-Clients Bilder) fiel im `default`-Zweig weg. Ein Vision-Modell hinter `/v1/messages` antwortete, als wäre kein Bild geschickt worden – still.
+- **Fix:** Bild-Blöcke werden zu OpenAI-`image_url`-Teilen (Data-URL für `base64`, die URL selbst für `url`-Quellen); eine Nachricht mit Bild bekommt Content als Teile-Array in Blockreihenfolge, Tool-Calls bleiben erhalten. Nachrichten ohne Bild unverändert. `spotbugs:check` grün.
+- **Reproduktion:** `AnthropicApiSupportTest#requestTurnsImageBlocksIntoImageUrlParts` (vorher rot, nachher 12/12); `OpenAiCompatServerHttpTest` 44/44 grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `3d38dd0`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J4-5):** Die `url`-Quelle und der Rückfall für eine andere Quelle hatten keinen Test (nur `base64`). `AnthropicApiSupportTest#requestTurnsUrlImageSourcesIntoImageUrlParts` pinnt die URL selbst als `image_url`, in einer Nachricht und in einem `tool_result`; `#anUnsupportedImageSourceLeavesTheMessageText` (Quelle `ftp`) die Textform der Nachricht und den Text eines `tool_result`, das nur ein solches Bild trägt. Gegenprobe rot: ohne den `url`-Zweig („Expected: is "image_url" but: was """), mit `url`-Übernahme für jede Quelle („Expected: is  but: was “); mit dem Code grün (`AnthropicApiSupportTest` 36/36). Die Quelle `file` (Files-API) und die Vorgabe `image/png` statt upstreams `image/jpeg` für ein `base64` ohne `media_type` sind nicht Teil dieses Nachtrags.
+- **Nachtrag (Branch-Review 2026-10-20, Fund JF-3):** Die Quelle `file` (Files-API, `{"type":"file","file_id":…}`) pinnt jetzt `AnthropicApiSupportTest#aFileImageSourceIsDroppedLikeUpstream`: in einer Nachricht und in einem `tool_result` entsteht kein `image_url`-Teil, die Nachricht behält ihre Textform (`what is this?`), das `tool_result` seinen Text (`done`), nichts wirft. Das ist upstreams Verhalten bei b11639: `server_chat_convert_anthropic_to_oai` (`tools/server/server-chat.cpp:417-441`, im `tool_result` `:478-494`; der Brief nannte `server-common.cpp`) kennt nur `base64` und `url` und verwirft alles andere still; der Kommentar im Test nennt die Stellen. Gegenprobe mit einer Mutation, die `imageUrl` für `file` eine leere URL liefern lässt (nicht committet): rot (`AnthropicApiSupportTest.aFileImageSourceIsDroppedLikeUpstream:210 Expected: is  but: was `, der Inhalt war ein Array mit `image_url`); am HEAD grün (37/37). Die Vorgabe für `base64` ohne `media_type` (`image/png` hier, `image/jpeg` upstream, `server-chat.cpp:423`/`:483`) bleibt eine Entscheidung, TODO.md beschreibt beide Punkte zutreffend.
+- **Nachtrag (Branch-Review 2026-10-21, Abschluss vor 6.0.0):** Ein `base64`-Bild ohne `media_type` heißt nicht mehr immer `image/png`: F256 erkennt PNG/JPEG/GIF/WebP an den Bytes und fällt wie upstream auf `image/jpeg` zurück.
+- **Nachtrag (Branch-Review 2026-10-21, Abschluss vor 6.0.0):** Die Entscheidung zur `file`-Quelle ist gefallen: F257 weist sie (und jede andere unbekannte Quelle sowie eine Quelle ohne `data`/`url`) mit 400 ab; `#aFileImageSourceIsDroppedLikeUpstream` und `#anUnsupportedImageSourceLeavesTheMessageText` sind durch `#aFileImageSourceIsRefused` und `#anImageSourceOfAnUnknownTypeIsRefused` ersetzt.
+
+
+### F30 – java-llama.cpp: Fehlgeschlagener `RpcServer.start` lässt den Server weiterlaufen, wenn der Stopp vor dem Bind kommt
+- **Zeit:** 06:34 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/RpcServer.java:526` (vorher: einmaliges `backend.stop()` in `awaitListening`)
+- **Schwere:** mittel
+- **Beschreibung:** Läuft der Start in den Timeout (z. B. langsame Geräte-Initialisierung) oder ist der aufrufende Thread unterbrochen, schickt `start` genau **einen** `stop()` und gibt den Single-Instance-Slot frei. Der native Server (patches/0015) setzt aber beim Binden des Sockets `g_rpc_server_stop = false` – ein Stopp vor dem Bind geht verloren. Der Server lauscht danach als verwaister Daemon-Thread weiter, obwohl `start` eine `LlamaException` geworfen hat; ein zweiter `start` belegt dann denselben globalen Server-Zustand. Beim unterbrochenen Aufrufer kehrte `join` zudem sofort zurück, statt zu warten. Fix: `stopAndJoin` wiederholt den Stopp alle 100 ms, solange der Thread lebt (bis `STOP_TIMEOUT_MILLIS`), und stellt das Interrupt-Flag danach wieder her.
+- **Reproduktion:** `RpcServerLifecycleTest#aStopThatArrivesBeforeTheBindDoesNotLeaveTheServerRunning` (Fake-Backend, das wie der native Server das Stopp-Flag beim Binden zurücksetzt; ohne Fix rot nach 31 s, mit Fix 15/15 grün)
+- **Status:** gefixt
+- **Commit:** e824ae5
+- **Nachtrag (Branch-Review 2026-10-20):** Die Wiederholung des Stopps war auf `STOP_TIMEOUT_MILLIS` begrenzt; überlebte der Thread sie, gab `start` den Einzelinstanz-Platz trotzdem frei und meldete „could not listen“. Seit F237 behält ein überlebender Thread den Platz, bis er endet, und `start` meldet ihn (`RpcServerLifecycleTest#aServerThatIgnoresTheStopIsReportedAndKeepsTheSlot`).
+
+
+### F36 – java-llama.cpp: `LlamaIterator` wird nach `cancel()` aus einem zweiten Thread durch ein spät eintreffendes Ergebnis wiederbelebt
+- **Zeit:** 2026-10-17 16:40 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/LlamaIterator.java:85` (`next`)
+- **Schwere:** mittel
+- **Beschreibung:** `next()` setzte nach jedem Ergebnis `hasNext = !output.stop`. Ruft ein anderer Thread `cancel()` auf, während `next()` in `receiveCompletionJson` auf das nächste Ergebnis wartet (das Feld ist genau dafür `volatile`, und die Slot-Arbeit läuft nach `cancel()` weiter), dann überschreibt das danach eintreffende Nicht-Stopp-Ergebnis das `false` von `cancel()` wieder mit `true`. Das Iterator „lebt“ wieder, `hasNext()` meldet weitere Tokens, und der nächste `next()` scheitert mit „Task not found“, weil `cancel()` den Reader schon freigegeben hat – entgegen dem Javadoc („the iterator stops yielding tokens immediately“). Betrifft `generate`/`generateChat` und jede `LlamaIterable`, die aus einem zweiten Thread abgebrochen wird.
+- **Fix:** `hasNext` geht nur noch von `true` nach `false`; `next()` schreibt `false` nur beim Stopp-Ergebnis und nie `true`. Für den Test bekommt der Iterator eine paketinterne Naht `LlamaIterator.TaskSource` (die fünf Task-Aufrufe, Adapter `TaskSource.of(LlamaModel)`); die JNI-Methoden bleiben in `LlamaModel`.
+- **Reproduktion:** `LlamaIteratorCancelTest#cancel_whileNextWaitsForAResultOnAnotherThread_iteratorStaysFinished` (der Fake hält `receiveCompletionJson` fest, bis `cancel()` gelaufen ist), dazu `#next_untilTheStopResult_releasesTheTaskOnceAndEnds`, `#close_beforeTheStopResult_cancelsTheTaskOnce`
+- **Rot-Grün-Nachweis:** Naht zuerst verhaltensneutral; dagegen rot 1/3 („a result that arrived after cancel() revived the iterator – Expected: is  but: was “). Mit Fix grün 3/3 (skipped 0); Gegenprobe mit dem Stand ohne Fix rot, wiederhergestellt grün. `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `79fa666`
+
+
+### F37 – java-llama.cpp: Ollama-Tool-Call mit leeren Argumenten kommt als `"arguments": null` statt `{}` an
+- **Zeit:** 2026-10-17 17:00 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java:345` (`parseArgumentsToObject`, genutzt von `toOllamaToolCalls` für `/api/chat` gestreamt und blockierend)
+- **Schwere:** niedrig
+- **Beschreibung:** Ollama erwartet `tool_calls[].function.arguments` als Objekt. Die Umwandlung aus dem OpenAI-String parste mit `readTree` und fiel nur bei einer `IOException` auf `{}` zurück (so der Kommentar). `readTree("")` wirft aber nicht, sondern liefert einen `MissingNode`, der als `null` serialisiert wird; ebenso ergab ein skalarer Text (`"5"`) einen Skalar. Ein parameterloser Aufruf, für den llama.cpp `"arguments": ""` liefert (oder dessen gestreamte Deltas keine Argumente enthalten, `ToolCallDeltaAccumulator` setzt dann `""`), kam bei Ollama-Clients als `"arguments": null` an. Fix: alles, was nicht zu einem Objekt oder Array parst, wird `{}`.
+- **Reproduktion:** `OllamaApiSupportTest#toOllamaChatResponseTurnsEmptyToolCallArgumentsIntoAnEmptyObject` (Jackson-Verhalten vorab per Probe bestätigt: `readTree("")` → `MissingNode` → `"arguments":null`)
+- **Rot-Grün-Nachweis:** ohne Fix rot 1/21 („Expected: is  but: was “: kein Objekt), mit Fix grün 21/21 (skipped 0); Gegenprobe per `git stash` rot, nach `stash pop` grün. `OpenAiCompatServerHttpTest` 44/44, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `2940edd`
+
+
+### F38 – java-llama.cpp: `JsonParameters` nimmt einen leeren oder nur aus Leerzeichen bestehenden Wert still an und sendet `"key":null`
+- **Zeit:** 2026-10-17 17:10 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/JsonParameters.java:181` (`parseOrThrow`)
+- **Schwere:** mittel
+- **Beschreibung:** `JsonParameters` garantiert laut CLAUDE.md, dass jeder gespeicherte Wert genau ein wohlgeformter JSON-Wert ist, geprüft beim Schreiben. Die Prüfung verließ sich darauf, dass `readTree` bei ungültiger Eingabe wirft. Für eine leere oder nur aus Leerzeichen bestehende Eingabe wirft auch der strikte Reader (`FAIL_ON_TRAILING_TOKENS`) nicht, sondern liefert einen `MissingNode` (per Probe bestätigt), der im Request als `"key":null` landete. `withJsonSchema("")`, `withResponseFormat("")`, `withToolsJson("")` usw. wurden so still angenommen, und die Generierung lief zum Beispiel ohne das Schema, das der Aufrufer zu setzen glaubte. Gefunden über die Erkenntnis aus F37 (`readTree("")` → `MissingNode`).
+- **Fix:** `parseOrThrow` weist einen `MissingNode` mit derselben `IllegalArgumentException` ab wie jeden anderen ungültigen Wert; die Meldung baut ein gemeinsamer Helfer `rejected(...)`.
+- **Reproduktion:** `JsonParametersTest#withRaw_rejectsEmptyAndBlankValues`
+- **Rot-Grün-Nachweis:** ohne Fix rot 1/25 („Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“), mit Fix grün 25/25 (skipped 0); Gegenprobe per `git stash` rot, nach `stash pop` grün. Paket `parameters` plus Server-Mapping- und HTTP-Tests: 775 Tests, 0 Fehler. PIT auf `JsonParameters` (Schwelle 100): alle Mutanten getötet. `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `2a5c835`
+
+
+### F39 – java-llama.cpp: Anthropic-`tool_use` mit leeren Argumenten bekommt `"input": null` statt `{}`
+- **Zeit:** 2026-10-17 17:15 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java:331` (`parseToObject`, genutzt für `tool_use.input` in `toAnthropicResponse`)
+- **Schwere:** niedrig
+- **Beschreibung:** Dasselbe Muster wie F37, gefunden in der `readTree`-Durchsicht nach F37: Die blockierende Antwort von `POST /v1/messages` wandelte die OpenAI-Argumente per `readTree` in `tool_use.input` und fiel nur bei einer `IOException` auf `{}` zurück. Für `"arguments": ""` (parameterloser Aufruf) entstand ein `MissingNode`, also `"input": null`; Anthropic verlangt dort ein Objekt, Clients wie das Anthropic-SDK lesen `input` als Dict. Fix: nur Objekt/Array durchreichen, sonst `{}`.
+- **Reproduktion:** `AnthropicApiSupportTest#toAnthropicResponseTurnsEmptyToolCallArgumentsIntoAnEmptyInputObject`
+- **Rot-Grün-Nachweis:** ohne Fix rot 1/13 („Expected: is  but: was “), mit Fix grün 13/13 (skipped 0); Gegenprobe per `git stash` rot, danach grün. `AnthropicStreamTranslatorTest` 4/4, `OpenAiCompatServerHttpTest` 44/44, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Durchsicht der übrigen `readTree`-Aufrufer (2026-10-17):** 35 Aufrufe in 24 Dateien. Betroffen waren nur F37 (Ollama), F38 (`JsonParameters`) und F39. Die übrigen lesen Antworten des eigenen Servers (Completion-/Chat-/Stream-Parser, Rerank, LoRA, Router) und greifen danach über `path(...)` zu, was auf einem `MissingNode` sicher leere/Default-Werte liefert; `ChatRequest.buildToolsJson` liest ein Tool-Schema, das ein leerer String zu `null` macht (bereits als F05-Umfeld bekannt), `StreamingChunkAssembler` wirft bei unparsebarem Chunk bewusst.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `6946b77`
+
+
+### F53 – java-llama.cpp: leerer Streaming-Umschlag beendet den Chat-Stream nicht
+- **Zeit:** 2026-10-17 21:00 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/json/ChatStreamChunkParser.java:58` (`feed`)
+- **Schwere:** niedrig
+- **Beschreibung:** Die Javadoc verspricht: „An unparseable envelope is treated as end-of-stream (returns true) so a polling loop cannot spin forever on malformed input.“ Für Nicht-JSON stimmt das (`IOException` → `true`), für einen leeren oder nur aus Leerraum bestehenden Umschlag nicht: `readTree` liefert dann `MissingNode` statt zu werfen (dieselbe Falle wie F37–F39), `path("stop")` ergibt `false`, und `LlamaModel`s Pollschleife (`chatStreamParser.feed` bei Zeile 823) läse weiter.
+- **Fix:** Nach dem Parsen gilt auch `root.isMissingNode()` als Streamende.
+- **Reproduktion:** `ChatStreamChunkParserTest#feed_emptyOrBlankEnvelope_reportsStop_andEmitsNothing` (`""`, `"   "`, `"\n"`)
+- **Rot-Grün-Nachweis:** ohne Fix rot („envelope '' Expected: is  but: was “), mit Fix grün; Gegenprobe per `git stash` rot, nach `stash pop` grün. `ChatStreamChunkParserTest` 7/7, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `4d19a04`
+
+
+### F54 – java-llama.cpp: leerer Wert in `chat_template_kwargs` wird zu `null` statt zum Text
+- **Zeit:** 2026-10-17 21:15 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/ParameterJsonSerializer.java:316` (`buildRawValueObject`)
+- **Schwere:** niedrig
+- **Beschreibung:** Erneute `readTree`-Durchsicht nach F53, jede Fundstelle mit `""` durchgespielt. Die Javadoc verspricht: „If a value cannot be parsed as JSON, it is stored as a JSON string literal.“ Für `"not json"` stimmt das, für einen leeren oder nur aus Leerraum bestehenden Wert nicht: `readTree` liefert `MissingNode`, und ein `MissingNode` als Kind eines `ObjectNode` wird als `null` geschrieben (in der Sandbox geprüft). `ModelParameters.setChatTemplateKwargs` und `InferenceParameters.withChatTemplateKwargs` gaben so `{"x":null}` an die Jinja-Vorlage weiter statt `{"x":""}`.
+- **Fix:** `MissingNode` wird wie ein Parsefehler behandelt und der Text unverändert als String eingetragen.
+- **Reproduktion:** `ParameterJsonSerializerTest#testBuildRawValueObject_emptyOrBlankValueFallsBackToString`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "{"empty":"","blank":"   "}" but: was "{"empty":null,"blank":null}"“), mit Fix grün; Gegenprobe per `git stash` rot, nach `stash pop` grün. `ParameterJsonSerializerTest` 35/35, `ModelParametersTest` 92/92, `InferenceParametersTest` 106/106, `WireNameRegistryTest` 8/8, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `3eb7a6e`
+
+
+### F55 – java-llama.cpp: leeres Tool-Parameterschema wird zu `"parameters": null`
+- **Zeit:** 2026-10-17 21:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/ChatRequest.java:329` (`buildToolsJson`)
+- **Schwere:** niedrig
+- **Beschreibung:** Aus der `readTree`-Durchsicht nach F53/F54. Ein nicht parsbares Parameterschema eines `ToolDefinition` wird bewusst als String weitergereicht (Test `buildToolsJsonDoesNotSilentlyTruncateASchemaWithTrailingContent`), damit es auffällt statt still verändert zu werden. Ein leeres oder nur aus Leerraum bestehendes Schema ergab dagegen `MissingNode` und damit `"parameters": null` – das Werkzeug ginge ohne Schema an die native Seite.
+- **Fix:** `MissingNode` nimmt denselben String-Rückfall wie jedes andere nicht parsbare Schema.
+- **Reproduktion:** `ChatRequestTest#buildToolsJsonKeepsAnEmptySchemaAsTheStringFallbackInsteadOfNull`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: a string containing "\"parameters\":\"\"" but: was "[{"type":"function","function":{"name":"f","description":"d","parameters":null}}]"“), mit Fix grün; Gegenprobe per `git stash` rot, nach `stash pop` grün. `ChatRequestTest` 25/25, `ToolCallingAgentTest` 6/6, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `8a13aa9`
+- **Nachtrag (Branch-Review 2026-10-20):** Der String-Rückfall war keine Verbesserung: upstream übernimmt `""` als Schema und weist es beim Grammatikbau mit „schema must be an object“ ab, ohne den Werkzeugnamen. Ersetzt durch F233 (Abweisung in `appendTool`); der Test oben ist entfallen.
+
+
+### F56 – java-llama.cpp: nicht darstellbarer Offline-Modellpfad ergibt `InvalidPathException` statt `ModelUnavailableException`
+- **Zeit:** 2026-10-17 22:10 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/loader/OfflineModelGuard.java:62` (`check`)
+- **Schwere:** niedrig
+- **Beschreibung:** Die Javadoc verspricht eine `ModelUnavailableException`, wenn `--offline` gesetzt ist und die konfigurierte lokale Modelldatei fehlt. Ein Pfad, den das Dateisystem nicht darstellen kann (NUL-Zeichen; unter Windows auch `?`, `*`, `<` …), ließ `Paths.get` eine `InvalidPathException` werfen – eine untypisierte `IllegalArgumentException`, noch bevor der native Lader erreicht wird. Ein solcher Pfad kann keine Datei bezeichnen, ist also genau der Fall „Datei fehlt“.
+- **Fix:** `localFileExists` behandelt eine `InvalidPathException` als „existiert nicht“.
+- **Reproduktion:** `LlamaModelOfflineTest#check_offlineAndUnrepresentablePath_throwsModelUnavailable`
+- **Rot-Grün-Nachweis:** ohne Fix rot („expected:  but was: “, Nul character not allowed), mit Fix grün; Gegenprobe per `git stash` rot, nach `stash pop` grün. `LlamaModelOfflineTest` 9/9, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `f3b8aed`
+- **Nachtrag (Branch-Review 2026-10-20):** Jede `InvalidPathException` als „fehlt“ zu werten war zu weit: unter Linux wirft `Paths.get` auch für einen Nicht-ASCII-Namen in einer JVM mit POSIX-Locale, obwohl die Datei existiert. Seit F238 gilt das nur noch für das NUL-Zeichen und für Namen, die die JVM kodieren kann (`OfflineModelGuardTest`); der Test oben bleibt grün.
+
+
+### F57 – java-llama.cpp: `ServerMetrics.getCumulativeUsage()` liefert ein `Usage` gegen dessen eigenen Vertrag
+- **Zeit:** 2026-10-17 21:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/value/ServerMetrics.java:203` (`getCumulativeUsage`)
+- **Schwere:** niedrig
+- **Beschreibung:** `Usage.getPromptTokens()` ist dokumentiert als „logical prompt token count, including cached tokens“, `getCachedTokens()` als dessen gecachte Teilmenge. `getCumulativeUsage()` füllt `promptTokens` aber mit `n_prompt_tokens_processed_total` (nur ausgewertete Tokens, ohne Cache-Treffer) und `cachedTokens` mit 0. Die Javadoc der Methode sagt das zwar, begründet es aber mit „upstream does not currently expose a cumulative logical or cached prompt total“ – das stimmt nicht mehr: `n_prompt_tokens_cached_total` kommt seit der `getMetrics()`-Zusammenführung mit und hat einen eigenen Getter (`getCumulativeCachedPromptTokens`). Ein Aufrufer, der `Usage` wie bei einer Chat-Antwort liest (`getPromptTokens()` gesamt, `getProcessedPromptTokens()` ausgewertet), bekommt hier zu kleine Gesamtzahlen.
+- **Historie:** Die Begründung „upstream does not currently expose a cumulative logical or cached prompt total“ steht in der erreichbaren Historie schon vor #494 (2026-10-09, flacher Klon, älter nicht sichtbar); `getCumulativeCachedPromptTokens()` und die Weitergabe von `n_prompt_tokens_cached_total` kamen mit `1eef547` (Bump auf b11538, 2026-10-09) – seitdem ist die Begründung überholt, `getCumulativeUsage()` wurde aber nicht nachgezogen. Bei b11639 nachgeprüft: upstream zählt in `prompt` nur ausgewertete Tokens und die Cache-Treffer getrennt (`server-common.h`, „cached ones are counted separately below“).
+- **Fix:** Entscheidung des Nutzers: `new Usage(processed + cached, predicted, cached)`; Javadoc korrigiert, `getWindowUsage()` dokumentiert, warum dort kein Cache-Anteil steht (upstream führt für das Fenster keinen). CHANGELOG unter „Fixed“ mit dem Hinweis für Aufrufer, die `getPromptTokens()` als „ausgewertet“ gelesen haben.
+- **Tests:** `ServerMetricsTest#cumulativeUsage` (schrieb das alte Verhalten fest, jetzt 800/700/100/1000 aus 100 ausgewerteten + 700 gecachten) und neu `#cumulativeUsage_readsLikeTheUsageOfAChatResponse` (10 + 4 → `new Usage(14, 3, 4)`).
+- **Rot-Grün-Nachweis:** ohne Fix rot (2 von 23), mit Fix grün (23 von 23).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `6853464`
+
+
+### F59 – java-llama.cpp: `FfmNativeServer.isRunning()` kann nach einem parallelen `close()` freigegebenen Speicher lesen
+- **Zeit:** 2026-10-17 21:25 UTC
+- **Datei:** `llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmNativeServer.java` (`isRunning`, `close`)
+- **Schwere:** niedrig
+- **Beschreibung:** `isRunning()` prüft `closed` und ruft dann `jllama_server_is_running(handle)`. `close()` setzt `closed` und ruft `jllama_server_stop`, das laut `jllama_api.h` den Handle freigibt. Läuft `close()` (etwa aus einem Shutdown-Hook) zwischen der Prüfung und dem nativen Aufruf eines anderen Threads, liest `jllama_server_is_running` einen freigegebenen `jllama_server` – undefiniertes Verhalten im nativen Code, schlimmstenfalls ein JVM-Absturz. Der JNI-Zwilling `server.NativeServer` hält genau deshalb `isRunning()` und `close()` unter einem gemeinsamen `lifecycleLock`; die FFM-Klasse dokumentiert keine Einschränkung. (`FfmLlamaModel` hat dasselbe Muster, schreibt aber ausdrücklich, dass `close()` keinen neuen Aufruf überholen darf – dort ein dokumentierter Vertrag, kein Fund.)
+- **Messung (J7, `BUGHUNT-MEASUREMENTS.md` in `4c58e8f`, JDK 25):** mit einer Naht rot – `close()` gab den Handle frei, während `isRunning()` noch im nativen Aufruf war; ohne Naht 862 Mio. Aufrufe in 30 Runden ohne sichtbaren Schaden (das Fenster ist schmal).
+- **Fix:** Entscheidung des Nutzers: das Lock aus `NativeServer`. `isRunning()` und `close()` laufen unter `lifecycleLock`, `closed` ist ein einfaches Feld unter diesem Lock. Die beiden nativen Aufrufe liegen hinter der paketinternen Naht `FfmNativeServer.Backend` (wie `NativeServer.Backend`), statt veränderlicher statischer Method-Handles.
+- **Tests:** `FfmNativeServerTest` (4, ohne Bibliothek): `close()` wartet auf ein `isRunning()` im nativen Aufruf und gibt erst danach frei; `isRunning()` nach `close()` fragt den Handle nicht ab; zweites `close()` ohne Wirkung; `isRunning()` fragt im offenen Zustand.
+- **Rot-Grün-Nachweis:** lokal mit JDK 25 (Temurin 25.0.1, Toolchain): ohne Lock (Naht beibehalten, `AtomicBoolean` wie vorher) rot (1 von 4, `close()` wartete nicht), mit Lock grün.
+- **Nachtrag (Branch-Review 2026-10-20):** Dieselbe Prüfen-dann-Rufen-Lücke hatte `FfmTextToSpeech`; dort mit demselben Lock als F198 behoben.
+- **Status:** gefixt
+- **Commit:** bd479b3
+
+
+### F60 – java-llama.cpp: Textblöcke einer Anthropic-Nachricht werden ohne Trenner zusammengeklebt
+- **Zeit:** 2026-10-17 22:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java:145` (`appendOpenAiMessages`, Fall `text`)
+- **Schwere:** niedrig
+- **Beschreibung:** `/v1/messages` übersetzt eine Nachricht mit mehreren `text`-Blöcken in einen OpenAI-String, hängte die Blöcke aber ohne Trenner aneinander. Anthropic-Clients teilen einen Zug regelmäßig in mehrere Blöcke (Hinweisblock, dann der eigentliche Text); an der Nahtstelle verschmolzen Wörter (`first` + `second` → `firstsecond`). llama.cpps eigenes `/v1/messages` behält die Blöcke als Content-Parts und verbindet sie beim Rendern mit einem Zeilenumbruch (`concat_content_parts` in `common/chat.cpp`, b11639). Der Bildpfad derselben Methode reicht bereits Parts weiter und war nicht betroffen; die System-Blöcke verbindet upstream ebenfalls ohne Trenner, sie bleiben unverändert.
+- **Fix:** Vor jedem weiteren Textblock ein `\n`, wie upstream.
+- **Reproduktion:** `AnthropicApiSupportTest#requestJoinsTheTextBlocksOfOneMessageWithANewline`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "first\nsecond" but: was "firstsecond"“, 14 Tests, 1 Fehlschlag), mit Fix grün 14/14; Gegenprobe per `git stash` rot, nach `stash pop` grün. `AnthropicStreamTranslatorTest` 4/4, `OpenAiCompatServerHttpTest` 44/44, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `29ba25c`
+- **Präzisierung (2026-10-17, 22:45 UTC):** Upstream verbindet Text-Parts nur dann mit `\n`, wenn die Chat-Vorlage ausschließlich String-Inhalt annimmt (`messages_inp_normalizer`, Zweig `only_string`, in `common/chat.cpp`); eine Vorlage mit typisiertem Inhalt bekommt die Parts als Array und rendert sie selbst, unter Umständen ohne Trenner. Die Übersetzung reicht aber keine Parts weiter, sondern einen fertigen String – sie muss sich also für einen Trenner entscheiden, und `\n` ist der, den upstream wählt, wenn es selbst verbindet. Der Fix bleibt damit sinnvoll; die frühere Aussage „upstream verbindet mit Zeilenumbruch“ galt nur für String-Vorlagen. Alternative für den Nutzer: Text-Blöcke wie Bilder als Content-Parts weiterreichen und die Entscheidung der Vorlage überlassen.
+
+
+### F61 – java-llama.cpp: Textteile eines Responses-Eingabeelements werden ohne Trenner zusammengeklebt
+- **Zeit:** 2026-10-17 22:15 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:183` (`inputContentText`)
+- **Schwere:** niedrig
+- **Beschreibung:** Musterdurchsicht nach F60. `/v1/responses` verband die `input_text`-Teile eines Eingabeelements ohne Trenner, sodass an der Naht Wörter verschmolzen (`first` + `second` → `firstsecond`). Upstreams `server_chat_convert_responses_to_chatcmpl` (b11639, `server-chat.cpp`) behält die Teile als Content-Parts, die `concat_content_parts` in `common/chat.cpp` mit einem Zeilenumbruch verbindet. Der Bildpfad (`inputContentWithImages`) reicht bereits Parts weiter und war nicht betroffen.
+- **Fix:** Vor jedem weiteren Textteil ein `\n`, wie upstream und wie F60.
+- **Reproduktion:** `ResponsesApiSupportTest#requestJoinsTheTextPartsOfOneInputItemWithANewline`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "first\nsecond" but: was "firstsecond"“, 11 Tests, 1 Fehlschlag), mit Fix grün 11/11; Gegenprobe per `git stash` rot, nach `stash pop` grün. `ResponsesStreamTranslatorTest` 4/4, `OpenAiCompatServerHttpTest` 44/44, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `196e76f`
+- **Präzisierung (2026-10-17, 22:45 UTC):** Upstream verbindet Text-Parts nur dann mit `\n`, wenn die Chat-Vorlage ausschließlich String-Inhalt annimmt (`messages_inp_normalizer`, Zweig `only_string`, in `common/chat.cpp`); eine Vorlage mit typisiertem Inhalt bekommt die Parts als Array und rendert sie selbst, unter Umständen ohne Trenner. Die Übersetzung reicht aber keine Parts weiter, sondern einen fertigen String – sie muss sich also für einen Trenner entscheiden, und `\n` ist der, den upstream wählt, wenn es selbst verbindet. Der Fix bleibt damit sinnvoll; die frühere Aussage „upstream verbindet mit Zeilenumbruch“ galt nur für String-Vorlagen. Alternative für den Nutzer: Text-Blöcke wie Bilder als Content-Parts weiterreichen und die Entscheidung der Vorlage überlassen.
+
+
+### F62 – java-llama.cpp: LangChain4j-Adapter klebt mehrere `TextContent` ohne Trenner zusammen
+- **Zeit:** 2026-10-17 22:20 UTC
+- **Datei:** `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java:261` (`toJllamaUserMessage`)
+- **Schwere:** niedrig
+- **Beschreibung:** Musterdurchsicht nach F60/F61. Eine `UserMessage` aus mehreren `TextContent` ohne Medien wird zu einem String ohne Trenner (`"Hello"` + `"world"` → `"Helloworld"`). Enthält dieselbe Nachricht zusätzlich ein Bild, bleiben die Teile Content-Parts, die llama.cpp mit `\n` verbindet (`concat_content_parts`) – ein Bild ändert also, wie die Texte zusammenkommen. Anders als bei F60/F61 gibt es kein upstream-Gegenstück als Maßstab, und der Test `LangChain4jMappingTest#flattensMultiTextUserMessageToText` schreibt das heutige Verhalten mit `"Hello "` (Leerzeichen am Ende) fest.
+- **Fix (Vorschlag, nicht umgesetzt):** wie F60/F61 mit `\n` verbinden (oder die Teile als Parts weiterreichen); dabei den festschreibenden Test anpassen. Entscheidung des Nutzers, weil der Test das Verhalten bewusst zu wählen scheint.
+- **Reproduktion:** `UserMessage.from(TextContent.from("Hello"), TextContent.from("world"))` → `getContent()` = `"Helloworld"`.
+- **Rot-Grün-Nachweis:** keiner (kein Fix ohne Entscheidung).
+- **Status:** gefixt als F94 (Duplikat: derselbe Befund wurde in der Musterdurchsicht nach F60/F61 noch einmal aufgenommen und dort behoben; Nachtrag 2026-10-19)
+- **Commit:** java-llama.cpp `980c48d` (F94)
+
+
+### F63 – java-llama.cpp: Responses-API-Objekte tragen `created_at: 0`
+- **Zeit:** 2026-10-17 22:25 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:286` (`newResponseShell`)
+- **Schwere:** niedrig
+- **Beschreibung:** `created_at` ist in der Responses-API und in llama.cpps eigenem `/v1/responses` (`to_json_oaicompat_resp`, b11639: `{"created_at", t}`) die Unix-Zeit der Erstellung. Die Schale, aus der jede Antwort und jedes Stream-Ereignis von `/v1/responses` entsteht, setzte die Konstante `0`; Clients zeigten jede Antwort als am 1. Januar 1970 erstellt. Die übrigen Oberflächen des Servers (`LlamaModelBackend`, Ollama) verwenden die aktuelle Zeit.
+- **Fix:** `System.currentTimeMillis() / 1000L`.
+- **Reproduktion:** `ResponsesApiSupportTest#responseCarriesTheCurrentUnixTimeAsCreatedAt`
+- **Rot-Grün-Nachweis:** ohne Fix rot („a value equal to or greater than <1792275470L> <0L> was less than <1792275470L>“, 12 Tests, 1 Fehlschlag), mit Fix grün 12/12; Gegenprobe per `git stash` rot, nach `stash pop` grün. `ResponsesStreamTranslatorTest` 4/4, `OpenAiCompatServerHttpTest` 44/44, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `fffb318`
+
+
+### F64 – java-llama.cpp: Ollama-Werkzeugaufrufe im Verlauf ohne `type` – llama.cpp lehnt die Anfrage ab
+- **Zeit:** 2026-10-17 22:35 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java:186` (`toOpenAiMessage`)
+- **Schwere:** mittel
+- **Beschreibung:** Ollama schickt die Werkzeugaufrufe einer Assistenten-Runde als `{"function": {"name", "arguments"}}` ohne `type`. Die Übersetzung nach OpenAI wandelte nur die Argumente in einen String und ließ `type` weg. llama.cpps OpenAI-Nachrichtenparser verlangt ihn aber für jeden Werkzeugaufruf (`throw std::invalid_argument("Missing tool call type: …")` in `common/chat.cpp`, b11639). Jede `/api/chat`-Anfrage, die einen früheren Werkzeugaufruf im Verlauf mitschickt – also jede zweite Runde eines Gesprächs mit Werkzeugen –, scheiterte deshalb mit 400, bevor sie das Modell erreichte. Der bestehende Test zur Argumentumwandlung prüfte genau diese Form, aber nur die Argumente.
+- **Fix:** Fehlt `type`, wird `"function"` gesetzt (der einzige Typ, den upstream annimmt).
+- **Reproduktion:** `OllamaApiSupportTest#toOpenAiChatRequestAddsTheFunctionTypeToAssistantToolCalls`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "function" but: was """, 22 Tests, 1 Fehlschlag), mit Fix grün 22/22; Gegenprobe per `git stash` rot, nach `stash pop` grün. `OpenAiCompatServerHttpTest` 44/44, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **CI-Bedarf:** der Ende-zu-Ende-Beweis (die 400 von upstream) braucht ein Modell; der modellgebundene `OpenAiCompatServerIntegrationTest` lief hier übersprungen.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `39b008b`
+
+
+### F65 – java-llama.cpp: CORS-Preflight erlaubt nur `Content-Type, Authorization` – Browser-SDKs werden abgewiesen
+- **Zeit:** 2026-10-17 23:10 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java` (`buildCorsFilter`)
+- **Schwere:** niedrig
+- **Beschreibung:** Bei gesetztem `corsAllowOrigin` beantwortete der Filter jeden Preflight mit `Access-Control-Allow-Headers: Content-Type, Authorization`. Der Server liest aber selbst `x-api-key` (F25) und bedient `/v1/messages`, dessen Browser-Clients `x-api-key`, `anthropic-version` und die `x-stainless-*`-Header der offiziellen SDKs schicken. Fehlt ein angefragter Header in der Antwort, verwirft der Browser die eigentliche Anfrage – ein im Browser laufender Anthropic-Client erreichte den Server also nie, obwohl CORS eingeschaltet war.
+- **Fix:** Der Filter spiegelt die `Access-Control-Request-Headers` des Preflights (der Origin bleibt das Tor); ohne angefragte Header gilt `Content-Type, Authorization, x-api-key, anthropic-version`.
+- **Reproduktion:** `OpenAiCompatServerHttpTest#optionsPreflightAllowsTheRequestedHeaders` (roher Socket, `OPTIONS /v1/messages` mit `content-type, x-api-key, anthropic-version, x-stainless-os`)
+- **Rot-Grün-Nachweis:** ohne Fix rot (Antwort `access-control-allow-headers: content-type, authorization`, 45 Tests, 1 Fehlschlag), mit Fix grün 45/45; Gegenprobe per `git stash` rot, nach `stash pop` grün. `OllamaApiSupportTest` 22/22, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `9958f31`
+
+
+### F67 – java-llama.cpp: `setLoraAdapters` verspricht ein Leeren der KV-Caches, das upstream auf diesem Weg nicht macht
+- **Zeit:** 2026-10-17 23:40 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/LlamaModel.java:1124` (Javadoc `setLoraAdapters`), ebenso `README.md:833`
+- **Schwere:** niedrig (Dokumentation; das Verhalten selbst ist upstreams)
+- **Beschreibung:** Javadoc und README sagen: „The native side clears affected KV caches when the effective adapter set changes.“ `setLoraAdapters` geht über `jllama_set_lora_adapters` → `engine::set_lora_adapters` → `SERVER_TASK_TYPE_SET_LORA`; upstream (`server-context.cpp`, b11639) setzt dort nur `params_base.lora_adapters = construct_lora_list(...)`. Eine spätere Anfrage ohne eigene `lora`-Angabe übernimmt in `launch_slot_with_task` im `else`-Zweig `slot.lora = params_base.lora_adapters`, **ohne** `slot.prompt.clear()` – den Cache leert upstream nur im Zweig für LoRA pro Anfrage (`lora_should_clear_cache`). Eine Anfrage, deren Prompt-Präfix noch im Slot liegt, rechnet also mit KV-Einträgen weiter, die unter den alten Adapter-Skalen entstanden sind, und liefert stillschweigend eine Mischung aus altem und neuem Adapter. Wer sich auf die Javadoc verlässt, rechnet damit nicht.
+- **Fix:** Entscheidung des Nutzers: Dokumentation korrigieren (ohne Test, Nutzerregel für Doku-Korrekturen). Javadoc von `LlamaModel.setLoraAdapters` und README sagen jetzt, dass die KV-Caches auf diesem Weg **nicht** geleert werden, und nennen `eraseSlot(id)` bzw. `withCachePrompt(false)`. Vor der Änderung am Quelltext von b11639 nachgeprüft: `SERVER_TASK_TYPE_SET_LORA` (`server-context.cpp:3077`) setzt nur `params_base.lora_adapters`, `launch_slot_with_task` (`:1868`) übernimmt sie im `else`-Zweig ohne `prompt.clear()`. Das Leeren in `engine::set_lora_adapters` (C++) bleibt als Möglichkeit, ist aber nicht beschlossen.
+- **Reproduktion:** keine lauffähige: es braucht ein Modell und einen dazu passenden LoRA-Adapter (nicht in `models.csv`), und in dieser Sandbox gibt es keine native Bibliothek und kein `gguf-py`. Ein Test würde denselben Prompt vor und nach `setLoraAdapter(0, 0f)` mit Greedy-Sampling schicken und mit einem frisch geladenen Modell ohne Adapter vergleichen.
+- **CI-Bedarf:** ein kleines Modell mit LoRA-Adapter im Modellsatz (oder ein zur Laufzeit erzeugtes Zufallsmodell samt Adapter, wie `test_c_api.cpp` es für das Modell macht) – nur, falls das Verhalten geändert werden soll.
+- **Status:** gefixt (zuerst Dokumentation, seit 2026-10-19 Patch 0021, siehe Nachtrag)
+- **Commit:** cb8a3ea
+- **Nachtrag 2026-10-19 (Verhalten geändert):** J9 hat F67 bestätigt (`4c58e8f`: nach `setLoraAdapter(0, 0f)` lieferte der warme Slot „. He saw a picture of a bird …“ statt „ was a little girl …“ wie ein geleerter Slot). Entscheidung des Nutzers: upstream-fähiger Patch `0021` statt eines Leerens in `engine::set_lora_adapters`, weil upstreams eigene HTTP-Route `POST /lora-adapters` denselben Fehler hat. Der `else`-Zweig von `launch_slot_with_task` wendet jetzt dieselbe Regel an wie LoRA pro Anfrage (`lora_should_clear_cache`, aLoRA behält den Cache). Das deckt auch den zweiten Weg ab, den die Durchsicht fand: eine Anfrage mit eigenen Adaptern füllt den Cache, die nächste ohne läuft mit den Server-Adaptern darauf weiter. Nicht abgedeckt, in Javadoc/README genannt: der Host-RAM-Prompt-Cache (`--cache-ram`, standardmäßig an) speichert Slot-Zustände ohne ihre Adapter. Javadoc `setLoraAdapters`, README, CHANGELOG, CLAUDE.md (Patch- und Testtabelle), `docs/patches.md`. Upstream-master bewusst nicht geprüft (Entscheidung des Nutzers: zeigt sich beim nächsten Bump über den Drop-Check).
+- **Tests (Patch):** `src/test/cpp/test_lora_cache.cpp` (4): Zufallsmodell + Zufalls-LoRA auf K, V und FFN-Ausgang, ein Slot, `--cache-ram 0`, greedy; nach Skala 1→0, 0→1 und nach einer Anfrage mit eigenen Adaptern muss der warme Slot wie ein geleerter antworten; ein vierter Fall schließt das leere Bestehen aus (Adapter ändert die Antwort, Wiederholung nutzt den Cache). Das Modellschreiben liegt jetzt in `tiny_llama_gguf.hpp`, gemeinsam mit `test_c_api.cpp`.
+- **Rot-Grün-Nachweis (Patch):** ohne Patch rot (3 von 4; z. B. „mYmq5*mq“ statt „C+oM~kM~“), mit Patch grün (4 von 4, drei Läufe); ganze C++-Suite 633/633; alle 21 Patches wenden sich der Reihe nach sauber auf b11639 an; `check-patches.py` 0 Probleme.
+- **Commit (Patch):** java-llama.cpp `e5a3efc`
+- **Nachtrag (Branch-Review 2026-10-20):** Der Schutz gegen das leere Bestehen prüfte `tokens_cached > 0`; das ist bei b11639 `slot.prompt.n_tokens()`, also jedes Token, das der Slot nach der Anfrage hält, und nach jeder Anfrage positiv, auch ohne ein einziges wiederverwendetes (Fund J12-1). Jetzt `timings.cache_n > 0` (upstreams `n_prompt_cached` = `n_past`) und `timings.prompt_n < tokens_evaluated`; die drei Wechselfälle verlangen zusätzlich `timings.cache_n == 0` für die Anfrage nach dem Wechsel, sind damit ohne den Patch rot, auch wo das Zufallsmodell zufällig gleich antwortet. Gegenprobe: mit `"cache_prompt": false` für die Wiederholung ist der neue Schutz rot (`Expected: (cache_n(again)) > (0), actual: 0 vs 0`, `prompt_n` 77 = `tokens_evaluated` 77), der alte grün; ohne den `else`-Hunk von `0021` sind die drei Fälle rot (`cache_n(after_change) Which is: 76`, dazu die Inhaltsprüfung); mit allem 5/5 grün. Beim Prüfen bemerkt (offen, nicht Teil dieses Nachtrags): `LoraCacheFreshSlot` schreibt `slot0.bin` ins Arbeitsverzeichnis des Testlaufs statt in sein `--slot-save-path`, weil die Slot-API einen Pfad nimmt (`engine::slot_action` setzt `filepath = filename`, wie `LlamaModel.saveSlot(int, String filepath)` es dokumentiert); die Datei bleibt dort liegen.
+
+
+### F68 – java-llama.cpp: `RouterClient.awaitModelLoaded` läuft bei einem schlafenden Modell in den Timeout
+- **Zeit:** 2026-10-17 23:55 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/RouterClient.java` (`awaitModelLoaded`)
+- **Schwere:** niedrig
+- **Beschreibung:** Mit `--sleep-idle-seconds` schläft ein geladener Worker im Leerlauf und wacht bei der nächsten Anfrage auf; `GET /models` meldet ihn dann als `sleeping`. Upstreams eigenes Warten auf ein Modell (`server_models`, `server-models.cpp:1501`/`1550`, b11639) behandelt `sleeping` ausdrücklich wie `loaded` („child is sleeping but still running; new request will wake it up“). `awaitModelLoaded` nahm nur `LOADED` an: wer ein bereits geladenes, aber leerlaufendes Modell abwartete, bekam nach der vollen Wartezeit `IllegalStateException` („did not reach LOADED … last status: sleeping“), obwohl das Modell bereit war.
+- **Fix:** `SLEEPING` zählt wie `LOADED` als bereit; Javadoc und README-Kommentar angepasst.
+- **Reproduktion:** `RouterClientTest#awaitModelLoaded_sleepingModelIsReady` (Stub-Server liefert `sleeping`)
+- **Rot-Grün-Nachweis:** ohne Fix rot (`IllegalStateException: Model 'qwen' did not reach LOADED within 2000 ms (last status: qwen [sleeping])`), mit Fix grün, `RouterClientTest` 19/19; Gegenprobe per `git stash` rot, nach `stash pop` grün. `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `70833e5`
+
+
+### F69 – java-llama.cpp: `withNPredict` dokumentiert `-2 = until context filled`, der Server lehnt `-2` ab
+- **Zeit:** 2026-10-18 00:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/InferenceParameters.java:197` (Javadoc `withNPredict`)
+- **Schwere:** niedrig (Dokumentation; der Aufrufer bekommt eine Ausnahme, kein stilles Fehlverhalten)
+- **Beschreibung:** Die Javadoc nennt für den Anfragewert `n_predict` „-1 = infinity, -2 = until context filled“. Upstream (`tools/server/server-schema.cpp:44`, b11639) deklariert das Feld mit `set_hard_limits(-1, INT32_MAX)`, und `is_hard_limit` heißt laut `server-schema.h:52` „throw error if the value is invalid“: `withNPredict(-2)` endet also in einer 400 bzw. `InvalidRequestException`, statt bis zum Kontextende zu generieren. `-2` gibt es nur für die Startoption `-n` – **Nachtrag:** dort ist es für den Server ebenfalls falsch dokumentiert, siehe F158.
+- **Fix:** Entscheidung des Nutzers: Dokumentation korrigieren (ohne Test). Die Javadoc von `withNPredict` sagt jetzt: `-1` übernimmt die `--predict`-Einstellung des Modells (selbst standardmäßig unbegrenzt, `server-context.cpp:1964`), der Server nimmt `-1` und größer an, `-2` lehnt er als ungültige Anfrage ab. Upstreams Beschreibung für `0` („no tokens will be generated but the prompt is evaluated“) ist bewusst **nicht** übernommen: im Quelltext findet sich dafür kein eigener Zweig, `has_budget()` stoppt erst nach dem ersten Token; ohne Messung bleibt die Aussage draußen.
+- **Reproduktion:** keine lauffähige hier (kein Modell, keine native Bibliothek). Ein Test in einer modellgestützten Klasse: `model.complete(InferenceParameters.empty().withPrompt("x").withNPredict(-2))` → `InvalidRequestException` mit `n_predict` in der Meldung.
+- **CI-Bedarf:** ein modellgestützter Java-Test (jedes Modell aus `models.csv` genügt), falls `-2` zusätzlich in Java abgewiesen werden soll.
+- **Nachtrag 2026-10-19 (geschlossen):** J2a hat die Ablehnung gemessen (`InvalidRequestException`, „Field 'n_predict': Value must be between -1 <= value <= 2147483647“). Entscheidung des Nutzers: kein Java-Check (er würde denselben Fehler nur früher melden und upstreams Grenze ein zweites Mal pflegen), dafür die Ablehnung modellgestützt festgeschrieben: `ErrorHandlingTest#testNPredictMinusTwoPerRequestIsAnInvalidRequest` (läuft in jedem `test-java-*`-Job mit dem CI-Modell). Lokal mit einem zufälligen Zwei-Schicht-GGUF geprüft: mit `-2` grün, als Gegenprobe mit `-1` rot („Expected InvalidRequestException to be thrown, but nothing was thrown“).
+- **Status:** gefixt (Dokumentation)
+- **Commit:** cb8a3ea
+
+
+### F70 – java-llama.cpp: `OpenAiServerCli` bewirbt mit `--parallel N` das ganze `-c` statt des Kontexts je Slot
+- **Zeit:** 2026-10-18 00:15 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiServerCli.java` (`Options.toServerConfig`)
+- **Schwere:** mittel
+- **Beschreibung:** Aus `-c` leitet die CLI die beworbenen Budgets ab: `maxInputTokens`/`maxOutputTokens` (die Copilot & Co. zum Zuschneiden des Prompts nutzen) und daraus die Kontextlänge in `/api/show` und `/props`. Bei expliziter `--parallel N` vereinheitlicht llama.cpp den KV-Cache nicht (`-kvu`: „default: enabled if number of slots is auto“, `common/arg.cpp`, b11639), jeder Slot und damit jede Anfrage bekommt `n_ctx / N`, und upstreams eigenes `/props` meldet genau diese Slot-Größe (`slot_n_ctx`, `server-context.cpp:5083`). Mit `-c 8192 --parallel 4` warb der Server aber 8192 Token: Clients füllten Prompts auf das Vierfache dessen, was eine Anfrage fassen kann, und liefen in „exceeds the available context size“.
+- **Fix:** Bei `parallel > 1` wird das Fenster je Slot (`ctx / parallel`) als Grundlage der Budgets genommen; ohne `--parallel` (Slotzahl „auto“, vereinheitlichter Cache) bleibt es das ganze `-c`.
+- **Reproduktion:** `OpenAiServerCliTest#parallelSlotsShareTheContextSoTheBudgetIsPerSlot`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is <2048> but: was <8192>“), mit Fix grün, `OpenAiServerCliTest` 39/39; Gegenprobe per `git stash` rot, nach `stash pop` grün. `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `657e792`
+
+
+### F71 – java-llama.cpp: Feintuning scheitert an jedem Zeichen außerhalb der BMP (Emoji) im Trainingstext
+- **Zeit:** 2026-10-18 00:35 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/TrainingParameters.java` (`toJson`), Gegenstück `llama/src/main/cpp/train_engine.cpp:141`
+- **Schwere:** niedrig
+- **Beschreibung:** `LlamaTrainer.finetune` übergibt `TrainingParameters.toJson()`, und die JNI-Schicht liest den String mit `GetStringUTFChars` – Modified UTF-8. Jackson schreibt Nicht-ASCII roh in die JSON, ein Zeichen außerhalb der BMP (Emoji, seltene CJK-Zeichen) kommt deshalb als zwei 3-Byte-Surrogate (CESU-8) beim Parser an. Das vendorte nlohmann am Pin lehnt das ab – gemessen mit `vendor/nlohmann/json.hpp` von b11639: echtes UTF-8 `F0 9F 98 80` wird gelesen, die CESU-8-Form `ED A0 BD ED B8 80` endet in `parse_error.101 … invalid string: ill-formed UTF-8 byte`. Der ganze Lauf scheiterte also mit „invalid training config“, sobald Trainingstext oder ein Pfad ein solches Zeichen enthielt. Die beiden anderen JNI-Stellen, die Text übernehmen (`jllama.cpp:248`, `native_server.cpp:33`), umgehen `GetStringUTFChars` ausdrücklich; nur der Trainer nicht.
+- **Fix:** `toJson` maskiert jedes Zeichen > 0x7F als `\uXXXX` (`escapeNonAscii`): ASCII ist in beiden Kodierungen gleich, und nlohmann setzt die Maskierungen samt Surrogatpaaren wieder zu richtigem UTF-8 zusammen. Bewusst nicht über `ESCAPE_NON_ASCII` am Mapper: `ObjectNode.toString()` nutzt Jacksons eigenen Mapper, und `writeValueAsString` hätte eine geprüfte Ausnahme eingeführt (SpotBugs `EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS`).
+- **Reproduktion:** `TrainingParametersTest#nonAsciiTextIsEscapedSoTheJsonCrossesJniIntact` (nur ASCII in der JSON, Rücklesen ergibt den Originaltext); der native Fehler selbst ist mit dem nlohmann-Probelauf oben belegt.
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is <0L> but: was <6L>“ Nicht-ASCII-Zeichen), mit Fix grün, `TrainingParametersTest` 8/8; Gegenprobe per `git stash` rot, nach `stash pop` grün. `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **CI-Bedarf:** ein Ende-zu-Ende-Beweis bräuchte `LlamaTrainerIntegrationTest` mit einem Emoji im Trainingstext (Modell `stories260K.gguf` liegt in CI vor).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `4867189`
+
+
+### F72 – java-llama.cpp: `GgufInspector` liest verschachtelte Arrays rekursiv – `StackOverflowError` statt `IOException`
+- **Zeit:** 2026-10-18 01:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/GgufInspector.java` (`readArray`)
+- **Schwere:** niedrig
+- **Beschreibung:** `readArray` liest den Elementtyp und ruft für jedes Element `readValue` – auch für den Typ `ARRAY`, also rekursiv und ohne Tiefengrenze. Upstreams Leser lehnt ein Array mit Elementtyp `ARRAY` dagegen ab (`ggml/src/gguf.cpp:606`, b11639: „key … has invalid GGUF type“), llama.cpp kann eine solche Datei also nicht laden. Folgen: (1) eine präparierte Datei mit tiefer Verschachtelung (12 Byte und ein Stapelrahmen je Ebene) beendet `GgufInspector.read` mit `StackOverflowError`, obwohl die Methode für eine ungültige Datei `IOException` deklariert – wer den Inspektor vor dem Laden als Prüfung nutzt (`catch (IOException)`), fängt das nicht; (2) schon zwei Ebenen meldet der Inspektor als gültige Metadaten.
+- **Fix:** `readArray` wirft bei Elementtyp `ARRAY` eine `IOException` mit dem Typ in der Meldung, wie upstream.
+- **Reproduktion:** `GgufInspectorTest#nestedArrayIsRejectedLikeUpstream` (2 Ebenen) und `#deeplyNestedArraysFailWithIOExceptionNotStackOverflow` (100 000 Ebenen)
+- **Rot-Grün-Nachweis:** ohne Fix rot („expected:  but was: “ bzw. „Expected java.io.IOException to be thrown, but nothing was thrown“), mit Fix grün, `GgufInspectorTest` 14 (1 übersprungen: der bestehende Test mit echtem Modell); Gegenprobe per `git stash` rot, nach `stash pop` grün. `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `5a0b834`
+
+
+### F74 – java-llama.cpp: Ollama-`/api/generate` mit `suffix` (FIM) verwirft `stop`, `top_p`, `top_k` und `seed`
+- **Zeit:** 2026-10-18 01:50 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java:403` (`toInfillRequest`)
+- **Schwere:** mittel
+- **Beschreibung:** Ein `/api/generate` ohne `suffix` übernimmt aus `options` `temperature`, `top_p`, `top_k`, `seed`, `num_predict` und `stop`. Mit `suffix`, also als Fill-in-the-Middle-Anfrage, übernahm `toInfillRequest` nur `temperature` und `num_predict`. Gerade FIM-Clients (Autovervollständigung über den Ollama-Provider) begrenzen ihre Vervollständigung mit `options.stop` (etwa `"\n\n"`); ohne sie lief jede Vervollständigung bis `num_predict` weiter. Upstreams `/infill` (`server-context.cpp`, `post_infill`, b11639) gibt den ganzen Body an `handle_completions_impl` und damit an denselben Schema-Parser wie `/completions`, liest also `stop`, `top_p`, `top_k` und `seed` genauso.
+- **Fix:** `toInfillRequest` übernimmt dieselben Optionen wie `toOpenAiCompletionRequest` (`num_predict` weiter als `n_predict`, dem Namen des nativen Infill-Bodys).
+- **Reproduktion:** `OllamaApiSupportTest#generateWithSuffixKeepsTheSamplingOptionsOfAPlainGenerate`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "\n\n" but: was ""“), mit Fix grün (23/23 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün. `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `b641834`
+
+
+### F75 – java-llama.cpp: Responses-API verwirft `reasoning.effort` – `"none"` schaltet das Denken nicht ab
+- **Zeit:** 2026-10-18 02:00 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:51` (`toOpenAiChatRequest`)
+- **Schwere:** mittel
+- **Beschreibung:** `toOpenAiChatRequest` baut einen neuen Chat-Request und übernimmt nur Nachrichten, Werkzeuge, `temperature`, `top_p` und `max_output_tokens`. Das Feld `reasoning: {"effort": …}` des Responses-Protokolls fiel weg. upstreams eigener Responses-Umsetzer (`server-chat.cpp`, `convert_responses_to_chatcmpl`, b11639) bildet es auf `reasoning_effort` ab, und der Chat-Parser (`server-common.cpp:1420`) liest es: `"none"` schaltet das Denken der Vorlage ab, ein anderer Wert geht als `reasoning_effort` an die Vorlage. Ein Responses-Client, der `effort: "none"` (oder `"low"`) schickt, bekam bei uns trotzdem den vollen Denkblock samt der Token, die er kostet.
+- **Fix:** ein textuelles `reasoning.effort` wird als `reasoning_effort` übernommen, wie upstream.
+- **Reproduktion:** `ResponsesApiSupportTest#requestMapsTheReasoningEffortLikeUpstream`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "none" but: was ""“), mit Fix grün (13/13 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün. `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Nachtrag (2026-10-18):** wirkt erst zusammen mit [F77](#f77): der Chat-Mapper, durch den jede übersetzte Anfrage läuft, verwarf das erzeugte Feld bis dahin.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `eeba74d`
+
+
+### F76 – java-llama.cpp: Anthropic-`/v1/messages` verwirft `thinking.budget_tokens` – das Modell denkt unbegrenzt
+- **Zeit:** 2026-10-18 02:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java:108` (`toOpenAiChatRequest`)
+- **Schwere:** mittel
+- **Beschreibung:** Ein Anthropic-Client begrenzt das Denken mit `thinking: {"type": "enabled", "budget_tokens": N}`. `toOpenAiChatRequest` übernahm `max_tokens`, `temperature`, `top_p`, `top_k` und `stop_sequences`, aber nicht `thinking`. upstreams `convert_anthropic_to_oai` (`server-chat.cpp:600`, b11639) setzt daraus `thinking_budget_tokens` (Vorgabe 10000 ohne `budget_tokens`), das der Chat-Parser (`server-common.cpp:1441`) als `reasoning_budget_tokens` an den Sampler gibt, der den Denkblock nach N Token schließt. Bei uns galt das Budget des Servers (Vorgabe unbegrenzt): ein Denkblock konnte die ganze Ausgabe aufbrauchen.
+- **Fix:** ein `thinking` mit `type: "enabled"` wird zu `thinking_budget_tokens` (`budget_tokens` oder upstreams Vorgabe 10000). Andere Typen (`disabled`) bleiben wie bei upstream ohne Wirkung.
+- **Reproduktion:** `AnthropicApiSupportTest#requestMapsAnEnabledThinkingBudgetLikeUpstream`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is <1024> but: was <0>“), mit Fix grün (15/15 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün. `AnthropicStreamTranslatorTest` 4/4, `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Nicht übernommen:** upstreams Vorgabe `max_tokens` 4096, wenn es fehlt (bei uns unbegrenzt; das Anthropic-Protokoll verlangt das Feld), und das Durchreichen von `chat_template_kwargs` – beide bleiben als offener Verdacht stehen.
+- **Nachtrag (2026-10-18):** wirkt erst zusammen mit [F77](#f77): der Chat-Mapper, durch den jede übersetzte Anfrage läuft, verwarf das erzeugte Feld bis dahin.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `262b41d`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J4-5):** Die Vorgabe 10000 und die Wirkungslosigkeit anderer Typen hatten keinen Test (nur `enabled` mit 1024). `AnthropicApiSupportTest#enabledThinkingWithoutBudgetUsesUpstreamsDefault` pinnt 10000, `#disabledThinkingSetsNoBudget` (`type` `disabled` mit `budget_tokens` 1024) das Fehlen von `thinking_budget_tokens`. Gegenprobe rot: Vorgabe 0 statt 10000 („Expected: is <10000> but: was <0>“), Budget für jeden `thinking`-Block statt nur `enabled` („Expected: is  but: was “); mit dem Code grün (`AnthropicApiSupportTest` 36/36).
+
+
+### F77 – java-llama.cpp: `OpenAiRequestMapper` verwirft `chat_template_kwargs`, `reasoning_effort` und das Reasoning-Budget jedes Chat-Requests
+- **Zeit:** 2026-10-18 02:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java:222` (`applyReasoningControls`, neu; Aufruf in `toInferenceParameters`)
+- **Schwere:** mittel
+- **Beschreibung:** Jede Chat-Anfrage an `OpenAiCompatServer` – OpenAI direkt sowie Ollama, Anthropic und Responses nach ihrer Übersetzung – läuft durch `OpenAiRequestMapper.toInferenceParameters`, das `InferenceParameters` aus einer festen Feldliste baut. Die Reasoning-Steuerungen, die upstreams Chat-Parser liest (`oaicompat_chat_params_parse`, `server-common.cpp:1403`–`1453`, b11639), fehlten darin: `chat_template_kwargs` (etwa `{"enable_thinking": false}`, mit dem Qwen3-Clients das Denken abschalten), `reasoning_effort` (`"none"` → Denken aus, sonst Template-Variable) und `reasoning_budget_tokens`/`thinking_budget_tokens`. Alle drei wurden still verworfen; `InferenceParameters` hat die passenden Setter (`withChatTemplateKwargs`, `withReasoningBudgetTokens`). Folge: die Fixes F75 (Responses `reasoning.effort`) und F76 (Anthropic `thinking.budget_tokens`) erzeugten das richtige Feld, das danach verloren ging.
+- **Fix:** `applyReasoningControls` übernimmt die Template-Variablen des Requests, setzt bei `reasoning_effort: "none"` `enable_thinking` auf `false` (und entfernt `reasoning_effort`), legt jeden anderen Wert als Template-Variable `reasoning_effort` ab und übernimmt das Budget (`reasoning_budget_tokens`, sonst `thinking_budget_tokens`) – die Regeln von upstreams Parser. Class-Javadoc nachgezogen.
+- **Reproduktion:** `OpenAiRequestMapperTest#chatTemplateKwargsAreForwarded`, `#reasoningEffortNoneSwitchesThinkingOffLikeUpstream`, `#reasoningEffortLevelBecomesATemplateVariableLikeUpstream`, `#reasoningBudgetIsForwardedUnderEitherUpstreamName`, `#anthropicAndResponsesReasoningControlsReachTheInferenceParameters` (die Kette Übersetzer → Mapper)
+- **Rot-Grün-Nachweis:** ohne Fix alle fünf rot (u. a. „Expected: is <512> but: was <0>“, „Expected: is "low" but: was ""“, „Expected: is <1024> but: was <0>“), mit Fix 21/21 der Klasse grün, skipped 0; Gegenprobe per `git stash` wieder 5 rot, nach `pop` grün. `AnthropicApiSupportTest` 15/15, `ResponsesApiSupportTest` 13/13, `OllamaApiSupportTest` 23/23, `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **CI-Bedarf:** ein modellgestützter Lauf (`OpenAiCompatServerIntegrationTest` mit dem Reasoning-Modell) würde zeigen, dass `reasoning_effort: "none"` den Denkblock wirklich unterdrückt; hier gibt es kein Modell.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `f6057ad`
+
+
+### F78 – java-llama.cpp: Ollama-`/api/chat` ignoriert `think` – `"think": false` schaltet das Denken nicht ab
+- **Zeit:** 2026-10-18 02:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java:143` (`toOpenAiChatRequest`)
+- **Schwere:** mittel
+- **Beschreibung:** Ollamas Chat-API steuert das Denken eines Modells mit dem Top-Level-Feld `think`: `false` schaltet es ab, `true` an, ein String wählt bei gpt-oss eine Stufe (`"low"`, `"medium"`, `"high"`). `OllamaApiSupport.toOpenAiChatRequest` las das Feld nicht. Ein Ollama-Client, der für schnelle Antworten `think: false` schickt (Open WebUI, ollama-python), bekam trotzdem einen vollen Denkblock samt seiner Token und Latenz. Gefunden in der Musterdurchsicht nach F77.
+- **Fix:** `think: false` wird zu `reasoning_effort: "none"`, ein nicht leerer String zu `reasoning_effort: `; `true` bleibt bei der Vorgabe der Vorlage (**korrigiert in F217:** `true` wird zu `chat_template_kwargs.enable_thinking: true`, sonst überging ein Server mit abgeschaltetem Denken die Anfrage still). Der Chat-Mapper wendet `reasoning_effort` seit F77 nach upstreams Regeln an (`enable_thinking: false` bzw. Template-Variable).
+- **Reproduktion:** `OllamaApiSupportTest#toOpenAiChatRequestHonoursThinkLikeOllama` (Kette Übersetzer → `OpenAiRequestMapper` → `InferenceParameters.toJson()`)
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is  but: was “ – `enable_thinking` fehlte), mit Fix grün (24/24 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün. `OpenAiRequestMapperTest` 21/21, `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `6374dc8`
+
+
+### F79 – java-llama.cpp: gestreamtes `/v1/completions` ignoriert `suffix`, `best_of` und `echo`, die der nicht gestreamte Weg abweist
+- **Zeit:** 2026-10-18 02:40 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java:89` (`toCompletionParameters`)
+- **Schwere:** niedrig
+- **Beschreibung:** Nicht gestreamt geht `/v1/completions` unverändert an `handleCompletionsOai`, also an upstreams `oaicompat_completion_params_parse` (`server-common.cpp:1082`, b11639), das `echo: true` („Only no echo is supported“) sowie `suffix` und `best_of` („Unsupported param: …“) mit 400 abweist. Gestreamt baut `toCompletionParameters` die Anfrage aus einer Teilmenge und überging diese Felder still. Ein gestreamter FIM-Request mit `suffix` lieferte so eine bloße Fortsetzung des Präfixes, die über den Suffix hinausläuft und ihn im Editor dupliziert, statt eines Fehlers; dieselbe Anfrage verhielt sich gestreamt und nicht gestreamt verschieden.
+- **Fix:** `toCompletionParameters` weist `echo: true`, `suffix` und `best_of` mit upstreams Meldungen ab (`IllegalArgumentException`, vom Server als Anfragefehler gemeldet).
+- **Reproduktion:** `OpenAiRequestMapperTest#completionParamsUpstreamRejectsAreRejectedOnTheStreamedPathToo`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“), mit Fix grün (22/22 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün. `OpenAiCompatServerHttpTest` 45/45, die drei Übersetzer-Tests, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Nachtrag (Branch-Review 2026-10-20):** Den Test oben gibt es nicht mehr: F186 (`4fb82c1`) hat `toCompletionParameters` entfernt und mit ihm `#completionParamsUpstreamRejectsAreRejectedOnTheStreamedPathToo`. Das gestreamte `/v1/completions` geht seitdem wie das blockierende durch upstreams `oaicompat_completion_params_parse`, das `echo: true`, `best_of` und `suffix` abweist; `completions_oai_stream_request` (`jllama_engine.cpp`) macht daraus `jllama_invalid_request` (400). Gehalten durch `OaicompatCompletionParams.EchoTrue_Throws`, `UnsupportedParam_BestOf_Throws` und `UnsupportedParam_Suffix_Throws` (`test_utils.cpp`) für den Parser und durch die Abweisungsfälle von `CApi.OaiCompletionsStreamDeliversTextCompletionChunksEndingWithUsage` (`test_c_api.cpp`, fehlender Prompt → `JLLAMA_STATUS_INVALID_REQUEST`) für die Abbildung auf 400.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `4a40a1d`
+
+
+### F80 – java-llama.cpp: Anthropic-Antwort verwirft den Denkinhalt – kein `thinking`-Block
+- **Zeit:** 2026-10-18 02:50 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java:297` (`toAnthropicResponse`)
+- **Schwere:** niedrig
+- **Beschreibung:** Die native OpenAI-Antwort trägt den Denkinhalt eines Reasoning-Modells in `message.reasoning_content`. `toAnthropicResponse` übernahm nur `content` und `tool_calls`, der Denkinhalt ging verloren. upstreams `to_json_anthropic` (`server-task.cpp:738`, b11639) setzt ihn als ersten Inhaltsblock `{"type": "thinking", "thinking": …, "signature": ""}`. Ein Anthropic-Client, der das Denken eingeschaltet hat (seit F76 auch wirksam), bekam es nie zu sehen; ein Client, der die Gedanken in der nächsten Runde zurückschickt, konnte das nicht.
+- **Fix:** ein nicht leeres `reasoning_content` wird wie upstream als erster Block `thinking` mit leerer Signatur ausgegeben.
+- **Reproduktion:** `AnthropicApiSupportTest#responseCarriesTheReasoningAsAThinkingBlockFirstLikeUpstream`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is <2> but: was <1>“), mit Fix grün (16/16 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün. `AnthropicStreamTranslatorTest` 4/4, `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Nicht im Fix:** der gestreamte Pfad (`AnthropicStreamTranslator`, upstream `thinking_delta`) und die Responses-Antwort (upstream ein `reasoning`-Item mit `reasoning_text`) – offener Verdacht.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `fdf21f1`
+
+
+### F81 – java-llama.cpp: gestreamte Anthropic-Antwort verwirft den Denkinhalt – keine `thinking_delta`s
+- **Zeit:** 2026-10-18 03:00 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicStreamTranslator.java:80` (`onChunk`, `end`), Ereignisse in `AnthropicApiSupport`
+- **Schwere:** niedrig
+- **Beschreibung:** Das Gegenstück zu F80 im Stream. Die nativen OpenAI-Chunks tragen den Denkinhalt als `delta.reasoning_content`; `AnthropicStreamTranslator` übersetzte nur `delta.content` und Werkzeugaufrufe. upstreams `to_json_anthropic_stream` (`server-task.cpp:1356`, `:907`, b11639) öffnet dafür einen Block `{"type": "thinking", "thinking": ""}`, schickt `thinking_delta`s und schließt ihn mit einem leeren `signature_delta`, das das Protokoll vor dem Schließen eines thinking-Blocks verlangt.
+- **Fix:** Der Übersetzer öffnet beim ersten `reasoning_content` einen thinking-Block, schickt die Teile als `thinking_delta` und schließt ihn mit `signature_delta` + `content_block_stop`, sobald Text beginnt oder der Stream endet; die Blöcke folgen einander, statt wie bei upstream bis zum Ende offen zu bleiben. Drei neue Ereignis-Bausteine in `AnthropicApiSupport`.
+- **Reproduktion:** `AnthropicStreamTranslatorTest#reasoningDeltasStreamAsAThinkingBlockClosedWithASignatureBeforeTheText`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: a string containing "\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}" but: was ""“), mit Fix grün (5/5 der Klasse, skipped 0); Gegenprobe per `git stash` beider Dateien rot, nach `pop` grün. `AnthropicApiSupportTest` 16/16, `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `7fdc684`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J4-2):** Die zweite Hälfte des Fixes („oder der Stream endet“, das `closeThinkingBlock` in `end()`) hatte keinen Test: der Test oben schließt den Block über den Text. `AnthropicStreamTranslatorTest#reasoningFollowedByAToolCallIsClosedBeforeTheToolUseBlock` (Denken, dann nur ein Werkzeugaufruf: `signature_delta` < `content_block_stop` von Block 0 < `tool_use`-Block mit Index 1, `stop_reason` `tool_use`) und `#aThinkingOnlyAnswerIsClosedAtTheEnd` (nur Denken: `end()` liefert `signature_delta` und den Stopp von Block 0 vor `message_delta`, öffnet keinen weiteren Block) pinnen ihn. Gegenprobe ohne das `closeThinkingBlock` in `end()` rot („signature_delta is emitted / Expected: is  but: was “, ebenso „end() emits the signature_delta“), mit ihm grün (`AnthropicStreamTranslatorTest` 11/11).
+- **Nachtrag (Branch-Review 2026-10-20, Fund J4-6):** Die Javadoc war hinter F80/F81 zurückgeblieben: die Klassen-Javadoc von `AnthropicApiSupport` nannte nur `text`-/`tool_use`-/`tool_result`-Blöcke und Antworten aus `text` + `tool_use`, die von `onChunk`/`end` nur den Textblock. Seit Commit `368baf5` nennt die Klassen-Javadoc `image`- und `thinking`-Blöcke (eine Nachricht mit Bild bleibt als `text`/`image_url`-Teile, auch in einem `tool_result`; `thinking` wird `reasoning_content`, `reasoning_content` in der Antwort ein führender `thinking`-Block mit leerer Signatur), `onChunk` sagt, dass `reasoning_content` einen thinking-Block öffnet (einen offenen Textblock vorher schließt) und der erste nicht leere Text ihn mit leerem `signature_delta` + `content_block_stop` schließt, `end()`, dass es einen noch offenen thinking-Block schließt. Reine Doku, kein Test.
+
+
+### F82 – java-llama.cpp: Responses-Antwort verwirft den Denkinhalt – kein `reasoning`-Item
+- **Zeit:** 2026-10-18 03:10 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:237` (`toResponsesResponse`)
+- **Schwere:** niedrig
+- **Beschreibung:** Gegenstück zu F80 für die Responses-API. `toResponsesResponse` übernahm aus der nativen OpenAI-Antwort `content` und `tool_calls`; `message.reasoning_content` ging verloren. upstreams `to_json_oaicompat_resp` (`server-task.cpp:531`, b11639) gibt ihn als erstes `output`-Item aus: `{"type": "reasoning", "summary": [], "content": [{"type": "reasoning_text", "text": …}], "encrypted_content": "", "status": "completed"}`. Ein Responses-Client sah die Gedanken nie, auch nicht, wenn er sie (seit F75/F77 wirksam) mit `reasoning.effort` angefordert hatte.
+- **Fix:** ein nicht leeres `reasoning_content` wird wie upstream als erstes `reasoning`-Item ausgegeben (Id `rs_`).
+- **Reproduktion:** `ResponsesApiSupportTest#responseCarriesTheReasoningAsAReasoningItemFirstLikeUpstream`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is <2> but: was <1>“), mit Fix grün (14/14 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün. `ResponsesStreamTranslatorTest` 4/4, `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `935342c`
+
+
+### F83 – java-llama.cpp: gestreamte Responses-Antwort verwirft den Denkinhalt – keine `response.reasoning_text.delta`s
+- **Zeit:** 2026-10-18 03:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesStreamTranslator.java:87` (`onChunk`, `end`)
+- **Schwere:** niedrig
+- **Beschreibung:** Gegenstück zu F82 im Stream. `ResponsesStreamTranslator` übersetzte `delta.content` und Werkzeugaufrufe, `delta.reasoning_content` ging verloren. upstreams `to_json_oaicompat_resp_stream` (`server-task.cpp:1198`, `:591`, b11639) öffnet ein `reasoning`-Item (`response.output_item.added`), streamt `response.reasoning_text.delta` und setzt das Item mit dem ganzen Text an den Anfang des abschließenden `output`.
+- **Fix:** Beim ersten Denkteil öffnet der Übersetzer ein `reasoning`-Item (`rs_`), streamt `response.reasoning_text.delta` mit `item_id`/`output_index`/`content_index` wie die Textdeltas, schließt mit `response.reasoning_text.done` und `response.output_item.done` und setzt das Item vorn in `response.completed.output`. Die beiden Delta-Ereignisse teilen sich einen Helfer (SpotBugs PRMC).
+- **Reproduktion:** `ResponsesStreamTranslatorTest#reasoningDeltasStreamAsAReasoningItemLikeUpstream`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: a string containing "event: response.output_item.added" but: was ""“), mit Fix grün (5/5 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün. `ResponsesApiSupportTest` 14/14, `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün (erst nach dem Helfer).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `f327156`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J5-6):** `output_index`, die Fortlaufendheit der `sequence_number` und das Ereignis `response.reasoning_text.done` hatten keinen Test; die Clients ordnen die Items nach `output_index` (das OpenAI-SDK wirft bei einem doppelten). `ResponsesStreamTranslatorTest#reasoningTextAndToolCallGetConsecutiveOutputIndicesAndSequenceNumbers` (begin, Denken, Text, Werkzeugaufruf, end) zerlegt die `data:`-Zeilen und verlangt genau 15 Ereignisse mit `sequence_number` 0..14, `output_item.added`/`.done` für `reasoning`/`message`/`function_call` an den Indizes 0/1/2, `response.reasoning_text.done` mit `item_id` `rs_resp_1`, `content_index` 0, vollem Text und `sequence_number` 6 sowie die Reihenfolge der Items in `response.completed`. Gegenprobe rot: Nachricht ohne eigenen Index („no response.output_item.added at output_index 2“), `sequence = 0` am Anfang von `end()` („Expected: is <[0, 1, …, 14]> but: was <[0, 1, 2, 3, 4, 5, 0, 1, …, 8]>“), ohne `reasoning_text.done` („Expected: is <15> but: was <14>“); mit dem Code grün (9/9). upstream (b11639) gibt weder `output_index` noch `sequence_number` aus; der Maßstab ist die OpenAI-Form.
+
+
+### F84 – java-llama.cpp: Anthropic-Verlauf verwirft `thinking`-Blöcke früherer Runden
+- **Zeit:** 2026-10-18 03:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java:166` (`appendOpenAiMessages`)
+- **Schwere:** niedrig
+- **Beschreibung:** Ein Anthropic-Client schickt die `thinking`-Blöcke früherer Assistenten-Runden im Verlauf mit (Pflicht bei Werkzeugschleifen mit Denken). `appendOpenAiMessages` ignorierte den Blocktyp; eine Runde nur aus Denkinhalt wurde zu `content: ""` ohne Gedanken. upstreams `convert_anthropic_to_oai` (`server-chat.cpp:415`, `:524`–`536`, b11639) sammelt die Blöcke als `reasoning_content` der Nachricht, das `common_chat_msgs_parse_oaicompat` liest und Vorlagen, die frühere Gedanken rendern (etwa bei verschachteltem Denken mit Werkzeugen), in den Prompt setzen. Bei uns sah das Modell einen anderen Verlauf als bei upstream.
+- **Fix:** `thinking`-Blöcke werden gesammelt und als `reasoning_content` an die Nachricht gehängt; eine Nachricht nur mit Denkinhalt wird wie upstream mit `content: ""` und `reasoning_content` übernommen. Der Chat-Mapper reicht `messages` unverändert weiter, das Feld erreicht also die native Seite.
+- **Reproduktion:** `AnthropicApiSupportTest#requestKeepsThinkingBlocksOfTheHistoryAsReasoningContentLikeUpstream`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "6 times 7" but: was ""“), mit Fix grün (17/17 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün. `AnthropicStreamTranslatorTest` 5/5, `OpenAiRequestMapperTest` 22/22, `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `638036f`
+
+
+### F85 – java-llama.cpp: Responses-`reasoning`-Eingabe wird zur Benutzernachricht
+- **Zeit:** 2026-10-18 03:40 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:199` (`appendReasoningItem`, neu; Aufruf in `appendInput`)
+- **Schwere:** mittel
+- **Beschreibung:** Ein Responses-Client schickt die `reasoning`-Items früherer Antworten im `input` zurück (Codex & Co. tun das in jeder Werkzeugschleife). `appendInput` kannte den Typ nicht; er fiel in den `default`-Zweig für Nachrichten, und weil ein reasoning-Item keine `role` hat, wurde daraus `role: "user"` mit dem Text seiner Inhaltsteile. Der frühere Gedankengang des Modells stand damit als Benutzereingabe im Prompt – das Modell sah den Nutzer seine eigenen Überlegungen sagen. upstreams `convert_responses_to_chatcmpl` (`server-chat.cpp:226`–`255`, b11639) hängt den Text als `reasoning_content` an die vorherige Assistenten-Nachricht (`merge_prev`) oder legt eine neue Assistenten-Nachricht an. Gefunden als Gegenstück zu F84.
+- **Fix:** `appendReasoningItem` übernimmt den Text von `content[0].text` als `reasoning_content` der letzten Assistenten-Nachricht, sonst einer neuen mit leerem `content`. Ein Item ohne lesbaren Text (nur `summary`/`encrypted_content`) wird übersprungen; upstream weist es mit 400 ab, hier wäre das für Clients mit verschlüsselten Gedanken eine unnötige Ablehnung.
+- **Reproduktion:** `ResponsesApiSupportTest#reasoningInputItemsBecomeAssistantReasoningNotUserTextLikeUpstream` (neue Assistenten-Nachricht nach einem Nutzer, Zusammenführung mit einem `function_call`, verschlüsseltes Item übersprungen)
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "q" but: was "plan"“ – der Denktext als Benutzernachricht), mit Fix grün (15/15 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün. `ResponsesStreamTranslatorTest` 5/5, `OpenAiRequestMapperTest` 22/22, `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün (nach Herausziehen zweier doppelter Aufrufe, PRMC).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `ad15d14`
+
+
+### F86 – java-llama.cpp: Responses-Werkzeugergebnis als Teil-Array kommt leer beim Modell an
+- **Zeit:** 2026-10-18 03:50 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:203` (`toolOutputParts`, neu; Aufruf in `appendInput`)
+- **Schwere:** mittel
+- **Beschreibung:** Ein `function_call_output` darf sein `output` als String oder als Array aus `input_text`-/`input_image`-Teilen schicken (z. B. ein Werkzeug, das einen Screenshot liefert). `appendInput` las `output.asText("")`; für ein Array ist das `""`, das Werkzeugergebnis erreichte das Modell also leer und es arbeitete ohne Ergebnis weiter. upstreams `convert_responses_to_chatcmpl` (`server-chat.cpp:189`–`220`, b11639) macht aus den Teilen `text`- und `image_url`-Teile der `role: tool`-Nachricht.
+- **Fix:** ein Array-`output` wird zu Inhaltsteilen (`input_text` → `text`, `input_image` → `image_url`), andere Teiltypen werden übersprungen (upstream weist sie ab); ein String bleibt wie bisher.
+- **Reproduktion:** `ResponsesApiSupportTest#functionCallOutputArrayKeepsItsPartsLikeUpstream`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "text" but: was ""“; die erste Fassung des Tests lief über `get(0)` in eine NPE und wurde auf `path(0)` umgestellt, damit der Fehler an der Assertion sichtbar wird), mit Fix grün (16/16 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün. `ResponsesStreamTranslatorTest` 5/5, `OpenAiRequestMapperTest` 22/22, `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Nachtrag (Branch-Review 2026-10-20):** Andere Teiltypen, ein Teil ohne `type` und ein Bild ohne URL werden seit F224 wie upstream mit 400 abgewiesen statt übersprungen; die Objektform `{url}` wird wie im Nachrichtenzweig gelesen.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `a2f8de1`
+
+
+### F87 – java-llama.cpp: Responses-`function_call`-Items werden einzelne Assistenten-Runden statt einer Runde mit mehreren Aufrufen
+- **Zeit:** 2026-10-18 04:00 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:198` (`assistantToolCalls`, neu; Aufruf in `appendInput`)
+- **Schwere:** niedrig
+- **Beschreibung:** Die Responses-API gibt parallele Werkzeugaufrufe als aufeinanderfolgende `function_call`-Items zurück, oft nach einem Text des Assistenten. `appendInput` legte für jedes Item eine eigene Assistenten-Nachricht an: aus „Text, Aufruf c1, Aufruf c2, Ergebnis c1, Ergebnis c2“ wurden drei Assistenten-Runden, und die Ergebnisse zu c1 folgten erst nach der Runde von c2. upstreams `convert_responses_to_chatcmpl` (`server-chat.cpp:177`–`187`, b11639) hängt jeden Aufruf an die vorherige Assistenten-Nachricht (`merge_prev`); die Vorlage sieht eine Runde mit zwei Aufrufen, wie sie das Modell erzeugt hat.
+- **Fix:** ein `function_call` hängt sich an die `tool_calls` der letzten Assistenten-Nachricht (das Array wird bei Bedarf angelegt), sonst entsteht eine neue Assistenten-Nachricht mit `content: null`. Folge wie bei upstream: ein `reasoning`-Item nach einem Aufruf ohne Ergebnis dazwischen überschreibt den Denktext derselben Nachricht (`prev_msg["reasoning_content"] = …`).
+- **Reproduktion:** `ResponsesApiSupportTest#consecutiveFunctionCallsJoinThePrecedingAssistantMessageLikeUpstream`; der F85-Test wurde auf eine realistische Werkzeugschleife umgestellt (Denktext, Aufruf, Ergebnis, Denktext, Antwort), weil seine erste Folge die getrennten Nachrichten der alten Variante voraussetzte.
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is <4> but: was <6>“, ebenso der umgestellte F85-Test), mit Fix grün (17/17 der Klasse, skipped 0); Gegenprobe per `git stash` rot (2 Fehlschläge), nach `pop` grün. `ResponsesStreamTranslatorTest` 5/5, `OpenAiRequestMapperTest` 22/22, `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `a4ac92a`
+
+
+### F88 – java-llama.cpp: Anthropic-`tool_result` verwirft Bilder
+- **Zeit:** 2026-10-18 04:10 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java:257` (`toolResultContent`, neu; Aufruf in `appendOpenAiMessages`)
+- **Schwere:** mittel
+- **Beschreibung:** Werkzeuge liefern ihr Ergebnis als `tool_result` mit Inhaltsblöcken, darunter Bilder (ein Screenshot, eine Bilddatei, die ein Agent wie Claude Code liest). `toolResultText` übernahm nur die Textblöcke; das Bild erreichte das Modell nie, ein Vision-Modell antwortete, als hätte das Werkzeug keins geliefert. upstreams `convert_anthropic_to_oai` (`server-chat.cpp:453`–`512`, b11639) macht aus einem Ergebnis mit Bildern Inhaltsteile `text`/`image_url` der `role: tool`-Nachricht und lässt nur ein reines Text-Ergebnis als String. Das Anthropic-Gegenstück zu F86, gefunden in der Durchsicht der Typ-Weichen gegen upstreams Liste.
+- **Fix:** `toolResultContent` gibt bei einem Bild die Teile in Blockreihenfolge zurück (`text` und `image_url` über das vorhandene `imageUrl`), sonst wie bisher den Text.
+- **Reproduktion:** `AnthropicApiSupportTest#toolResultImagesReachTheToolMessageLikeUpstream`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "cat.png" but: was ""“ – der Inhalt war ein String), mit Fix grün (18/18 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün. `AnthropicStreamTranslatorTest` 5/5, `OpenAiRequestMapperTest` 22/22, `OpenAiCompatServerHttpTest` 45/45, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün (nach `new TextNode(...)` statt `textNode(...)`, AI_ANNOTATION_ISSUES_NEEDS_NULLABLE).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `3ddf132`
+
+
+### F89 – java-llama.cpp: Responses-`input_file` wird still verworfen statt abgewiesen
+- **Zeit:** 2026-10-18 04:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:240` (`rejectInputFiles`, neu), `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java:703` (`handleResponses`)
+- **Schwere:** niedrig
+- **Beschreibung:** Hängt ein Responses-Client eine Datei als Inhaltsteil `input_file` an, übernahmen `inputContentText`/`inputContentWithImages` nur Text- und Bildteile; die Datei fiel weg und das Modell antwortete, als wäre sie nie geschickt worden (eine „Zusammenfassung“ ohne Dokument). upstreams `convert_responses_to_chatcmpl` (`server-chat.cpp:92`, b11639) antwortet darauf mit 400 „'input_file' is not supported by llamacpp at this moment“. Beim Fix zeigte sich zudem: `handleResponses` rief den Übersetzer außerhalb der Fehlerbehandlung auf, eine dort geworfene `IllegalArgumentException` hätte den Handler ohne Antwort verlassen.
+- **Fix:** `rejectInputFiles` wirft bei einem `input_file`-Teil eine `IllegalArgumentException` mit upstreams Meldung (plus Datei-Kennung, SpotBugs WEM); `handleResponses` meldet eine Ablehnung des Übersetzers als 400.
+- **Reproduktion:** `OpenAiCompatServerHttpTest#responsesWithAnInputFileIsRejectedLikeUpstream`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is <400> but: was <200>“), mit Fix grün (46/46 der Klasse, skipped 0); Gegenprobe per `git stash` beider Dateien rot, nach `pop` grün. `ResponsesApiSupportTest` 17/17, `ResponsesStreamTranslatorTest` 5/5, `OpenAiRequestMapperTest` 22/22, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `576a886`
+
+
+### F90 – java-llama.cpp: Claude Codes wechselnder `cch`-Stempel im System-Prompt verhindert jeden Prefix-Cache-Treffer
+- **Zeit:** 2026-10-18 04:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java:144` (`normalizeBillingHeader`, neu; Aufruf in `systemText`)
+- **Schwere:** mittel (Leistung)
+- **Beschreibung:** Claude Code beginnt seinen System-Prompt mit `x-anthropic-billing-header: cc_version=…; cc_entrypoint=cli; cch=a5145;…`, und der `cch`-Wert ändert sich bei jeder Anfrage. Der Prompt unterscheidet sich damit schon in den ersten Token, und der KV-Präfix-Cache (`cache_prompt`, vom Mapper auf `true` gesetzt) trifft nie: jede Runde einer Agenten-Sitzung rechnete den ganzen, viele Tausend Token langen System-Prompt samt Verlauf neu. upstreams `server_chat_convert_anthropic_to_oai` ruft dafür `normalize_anthropic_billing_header` (`server-chat.cpp:326`, llama.cpp #21793, b11639) und überschreibt die fünf Stempelzeichen mit `fffff`.
+- **Fix:** `normalizeBillingHeader` macht dasselbe wie upstream (nur bei diesem Präfix, nur wenn nach den fünf Zeichen ein `;` folgt), angewendet auf den String- und jeden Text-Block des `system`-Felds.
+- **Reproduktion:** `AnthropicApiSupportTest#billingHeaderStampIsNormalizedSoThePrefixCacheHitsLikeUpstream` (Block- und String-Form, zwei verschiedene Stempel ergeben denselben Text)
+- **Rot-Grün-Nachweis:** ohne Fix rot (Stempel `a5145` unverändert), mit Fix grün (19/19 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün. `AnthropicStreamTranslatorTest` 5/5, `OpenAiRequestMapperTest` 22/22, `OpenAiCompatServerHttpTest` 46/46, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **CI-Bedarf:** dass der Cache danach tatsächlich trifft (`cached_tokens` > 0 in der zweiten Runde), zeigt nur ein modellgestützter Lauf.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `70779b3`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J4-5):** Die Wächter („nur wenn nach den fünf Zeichen ein `;` folgt“) hatten keinen Test; der Test oben nimmt nur wohlgeformte Kopfzeilen. `AnthropicApiSupportTest#aSystemTextThatIsNoWellFormedBillingHeaderIsLeftAlone` gibt `normalizeBillingHeader` einen Text ohne Kopfzeile, eine Kopfzeile ohne `cch=`, einen Stempel am Textende und einen Stempel ohne folgendes `;` und verlangt jeden unverändert zurück. Gegenprobe rot: ohne `end >= length` („StringIndexOutOfBounds Index 56 out of bounds for length 56“), ohne `charAt(end) != ';'` („Expected: is "x-anthropic-billing-header: cch=a5145 You are Claude Code." but: was "x-anthropic-billing-header: cch=fffff You are Claude Code."“); der Wächter `stamp < 0` ist eine äquivalente Mutante (ohne ihn liegt `end` im festen Präfix, dessen Zeichen dort kein `;` ist), kein Test kann ihn unterscheiden. Mit dem Code grün (`AnthropicApiSupportTest` 36/36). Wie upstream bleibt der Text still unverändert; upstream protokolliert zusätzlich `LOG_ERR`.
+
+
+### F91 – java-llama.cpp: Responses-Verlauf spaltet jede Assistenten-Runde in Denk- und Antwortnachricht
+- **Zeit:** 2026-10-18 04:40 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:257` (`joinContent`, `lastAssistant`, neu; Aufruf im `message`-Zweig von `appendInput`)
+- **Schwere:** niedrig
+- **Beschreibung:** Ein Responses-Client gibt jede frühere Runde als `reasoning`-Item und danach als `message`-Item mit `role: assistant` zurück. Seit F85 wird das reasoning-Item zu einer Assistenten-Nachricht mit `reasoning_content`; das folgende `message`-Item legte dann eine **zweite** Assistenten-Nachricht mit der Antwort an. Die Vorlage sah jede Runde als zwei aufeinanderfolgende Assistenten-Runden, eine ohne Antwort und eine ohne Denkinhalt. upstreams `convert_responses_to_chatcmpl` (`server-chat.cpp:150`, b11639) führt ein Assistenten-`message`-Item mit der vorherigen Assistenten-Nachricht zusammen (`merge_prev`), wie bei Werkzeugaufrufen (F87). Gefunden beim Lesen des upstream-Umsetzers von oben bis unten (Regel aus F90).
+- **Fix:** Folgt ein Assistenten-`message`-Item auf eine Assistenten-Nachricht, wird sein Inhalt angehängt: ein leerer oder `null`-Inhalt ersetzt, zwei Texte mit Zeilenumbruch verbunden (wie `concat_content_parts`), sonst Teil-Arrays zusammengefügt.
+- **Reproduktion:** `ResponsesApiSupportTest#anAssistantMessageJoinsThePrecedingAssistantTurnLikeUpstream`; der F85-Test erwartet seine abschließende Antwort jetzt in der Nachricht mit dem zweiten Denktext.
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is <3> but: was <4>“, ebenso der angepasste F85-Test), mit Fix grün (18/18 der Klasse, skipped 0); Gegenprobe per `git stash` rot (2 Fehlschläge), nach `pop` grün. `ResponsesStreamTranslatorTest` 5/5, `OpenAiRequestMapperTest` 22/22, `OpenAiCompatServerHttpTest` 46/46, `LlamaArchitectureTest` 13/13, `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `8b0872b`
+
+
+### F92 – java-llama.cpp: blockierende LangChain4j-Antwort verliert den Denkinhalt
+- **Zeit:** 2026-10-18 05:10 UTC
+- **Datei:** `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java:206` (`toAiMessage`)
+- **Schwere:** niedrig
+- **Beschreibung:** `JllamaChatModel` (blockierend) baute die `AiMessage` nur aus Text und Werkzeugaufrufen; der `reasoning_content` der nativen Antwort ging verloren, `AiMessage.thinking()` war immer `null`. Der Streaming-Pfad (`StreamingChunkAssembler`) setzt ihn, und die Modul-README sagt, Reasoning erscheine als `AiMessage.thinking()`. Ein Wechsel von `JllamaStreamingChatModel` zu `JllamaChatModel` ließ den Denkinhalt also stillschweigend verschwinden.
+- **Fix:** `toAiMessage` liest den Denkinhalt mit `ChatResponseParser.extractChoiceReasoningContent` aus dem rohen JSON und setzt ihn in beiden Zweigen (mit und ohne Werkzeugaufrufe), wenn er nicht leer ist.
+- **Reproduktion:** `LangChain4jMappingTest#blockingResponseCarriesTheThinkingLikeTheStreamedOne`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "6 times 7"“), mit Fix grün (21/21 der Klasse, skipped 0); Gegenprobe per `git stash` rot, nach `pop` grün; alle Modultests grün (Integrationstests ohne Modell übersprungen). Das Modul deklariert weder Spotless noch SpotBugs (weder eigene noch Eltern-POM), diese Gates entfallen hier.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `7a64dd9`
+
+
+### F93 – java-llama.cpp: gestreamte LangChain4j-Antwort hat nie eine Token-Nutzung
+- **Zeit:** 2026-10-18 04:45 UTC
+- **Datei:** `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java:105` (`toStreamingParameters`)
+- **Schwere:** niedrig
+- **Beschreibung:** `StreamingChunkAssembler` übernimmt die Token-Nutzung aus dem abschließenden `usage`-Chunk. upstream (`server_task_result_cmpl_final::to_json_oaicompat_chat_stream`, `server-task.cpp:494`, b11639) sendet diesen Chunk aber nur bei `stream_options.include_usage`, und `toStreamingParameters` setzte das nie. Die gestreamte `ChatResponse` von `JllamaStreamingChatModel` hatte daher immer `tokenUsage() == null`, die blockierende dagegen die echte Zählung. Die Modul-README verspricht beides („both the blocking and the streamed final response carry the model's real finish reason and token usage“). Gefunden über den Vergleich blockierender und gestreamter Pfad (Erkenntnis aus F92).
+- **Fix:** `toStreamingParameters` setzt `stream_options` `{"include_usage":true}` vor dem Customizer (der es weiterhin überschreiben kann). `ChatStreamChunkParser` reicht den Usage-Chunk des Endschritts bereits einzeln weiter, der Assembler liest ihn.
+- **Reproduktion:** `LangChain4jMappingTest#streamingParametersRequestTheUsageChunk`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is  but: was “), mit Fix grün (22/22 der Klasse, skipped 0); Gegenprobe per `git stash` rot (1 Fehlschlag), nach `pop` grün; alle Modultests grün (`StreamingChunkAssemblerTest` 7/7, `JsonSchemaElementSerializerTest` 8/8, Integrationstests ohne Modell übersprungen). Spotless/SpotBugs sind im Modul nicht deklariert (siehe F92).
+- **CI-Bedarf:** Der Integrationsjob `test-java-llama-langchain4j-integration` läuft mit Modell; ein Ende-zu-Ende-Nachweis der Zählung im Stream wäre dort möglich, ist aber nicht Voraussetzung.
+- **Nachtrag (Branch-Review 2026-10-20):** Der Fix prüfte nur die Anfrageseite; kein Test fütterte den Usage-Chunk in der Form, die upstream mit `include_usage` sendet – ein eigener letzter Chunk mit leerem `choices`-Array, `usage` und `timings` nach dem Ende-Chunk (Fund J14-3). `StreamingChunkAssemblerTest#usageChunkWithEmptyChoicesCarriesTokenUsage` füttert genau die b11639-Folge (Inhalt, Ende-Chunk mit `finish_reason` und leerem `delta`, Usage-Chunk mit `id`/`model`/`object`/`system_fingerprint`) und pinnt 7/3 Token, das Teilstück `ok` und dass der Endegrund den Chunk ohne Wahl übersteht. Endegrund ist `length`, nicht `stop` wie im Vorschlag: ein fehlender Endegrund wird auf `STOP` abgebildet, mit `stop` bliebe ein Zurücksetzen unsichtbar. Ein reiner Absicherungstest ohne Verhaltensänderung, rot nur unter Mutation: `chunk.get("choices").get(0)` vor dem Lesen der Nutzung bzw. ein frühes `return` bei leerem `choices` (`Expected: is not null` auf `tokenUsage()`; ohne die bei der Prüfung ergänzte `notNullValue()`-Assertion war es eine `NullPointerException`), ein Zurücksetzen des Endegrunds auf einem Chunk ohne Wahl (`Expected: is  but: was `); unverändert 13/13 grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `6c2adf8`
+
+
+### F94 – java-llama.cpp: Textteile einer LangChain4j-Nutzernachricht werden ohne Trenner zusammengeklebt
+- **Zeit:** 2026-10-18 04:55 UTC
+- **Datei:** `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java:287` (`toJllamaUserMessage`)
+- **Schwere:** niedrig
+- **Beschreibung:** Musterdurchsicht nach F60/F61 im LangChain4j-Adapter. Eine `UserMessage` mit mehreren `TextContent`-Teilen und ohne Medien wurde zu einem String verbunden, aber ohne Trenner: `first` + `second` → `firstsecond`. LangChain4j-Werkzeuge (RAG-Kontext plus Frage, Vorlagen) teilen Nutzerzüge regelmäßig so auf. upstream verbindet Textteile für Vorlagen ohne typisierten Inhalt mit `\n` (`concat_content_parts`, `common/chat.cpp`, b11639); der Adapter reicht keine Parts weiter, sondern muss selbst einen Trenner wählen. Der bestehende Test `flattensMultiTextUserMessageToText` schrieb das Verhalten nur mit einem Leerzeichen am Ende des ersten Teils fest, das den Fehler verdeckte.
+- **Fix:** Verbinden mit `StringJoiner("\n")`, wie bei F60 (Anthropic) und F61 (Responses). Der festschreibende Test erwartet jetzt `"Hello \nworld"`.
+- **Reproduktion:** `LangChain4jMappingTest#joinsTextPartsWithANewlineLikeUpstream`, dazu `#flattensMultiTextUserMessageToText`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "first\nsecond" but: was "firstsecond"“, ebenso der angepasste Test), mit Fix grün (23/23 der Klasse, skipped 0); Gegenprobe per `git stash` rot (2 Fehlschläge), nach `pop` grün; alle Modultests grün (Integrationstests ohne Modell übersprungen). Spotless/SpotBugs sind im Modul nicht deklariert (siehe F92).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `980c48d`
+
+
+### F95 – java-llama.cpp: gestreamtes `/v1/completions` und der Chat-Pfad verwerfen llama.cpps Sampling-Felder
+- **Zeit:** 2026-10-18 05:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java:200` (`applyLlamaCppSampling`, neu; `readMaxTokens`)
+- **Schwere:** mittel
+- **Beschreibung:** Aus dem offenen Verdacht vom 2026-10-18 (gestreamtes `/v1/completions`). Das nicht gestreamte `/v1/completions` reicht den Körper an upstreams Schema durch (`server-schema.cpp`, b11639), das gestreamte baut `InferenceParameters` im `OpenAiRequestMapper` aus einer festen Feldliste. Alles außerhalb dieser Liste ging verloren: `min_p`, `typical_p`, `repeat_penalty`, `repeat_last_n`, `top_n_sigma`, `xtc_*`, `dynatemp_*`, `min_keep`, `n_keep`, `ignore_eos`, `grammar`/`json_schema`, und `n_predict` (upstreams Name, `max_tokens`/`max_completion_tokens` sind seine Aliase und verlieren gegen ihn). Dieselbe Anfrage erzeugte gestreamt also andere Ausgaben, eine Grammatik wirkte nur ohne `stream`. Derselbe Mapper bedient beide Chat-Pfade, die Felder fehlten dort ebenso. Alle Felder haben bereits einen Setter; anders als im Verdacht befürchtet, braucht dieser Teil keinen API-Umbau.
+- **Fix:** `applyLlamaCppSampling` (aus `applyCommonFields`) übernimmt die Felder mit den vorhandenen Settern; `json_schema` nur ohne `grammar`, wie upstream (**korrigiert in F218:** das gilt nur für upstreams Completion-Schema; der Chat-Parser, den der Mapper seit F186 allein bedient, weist das Paar mit 400 ab, also werden beide Felder durchgereicht). `readMaxTokens` liest zuerst `n_predict`. Weiter offen (Verdacht bleibt): `logit_bias`, `n_probs`/`logprobs` (im gestreamten Completion-Chunk ohnehin nicht ausgegeben) und `n`.
+- **Reproduktion:** `OpenAiRequestMapperTest#llamaCppSamplingExtensionsReachTheStreamedCompletion`, `#nPredictWinsOverMaxTokensLikeUpstream`, `#jsonSchemaReachesTheStreamedCompletionUnlessAGrammarIsGiven`
+- **Rot-Grün-Nachweis:** ohne Fix rot (3 Fehlschläge: „Expected: is a numeric value within <1.0E-4> of <0.05> but: <0.0>“, „Expected: is <7> but: was <50>“, „Expected: is "object" but: was ""“), mit Fix grün (25/25 der Klasse, skipped 0); Gegenprobe per `git stash` rot (3), nach `pop` grün. `OpenAiCompatServerHttpTest` 46/46, `AnthropicApiSupportTest` 19/19, `ResponsesApiSupportTest` 18/18, `OllamaApiSupportTest` 24/24, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün (eine erste Fassung mit allen Feldern in einer Methode meldete `CE_CLASS_ENVY`, eine Map-Tabelle `DMC_DUBIOUS_MAP_COLLECTION`; jetzt je Feld ein Aufruf über `withFloat`/`withInt`).
+- **Nachtrag (Branch-Review 2026-10-20):** Das hier „weiter offen“ Genannte ist erledigt: `logit_bias` und `n_probs` mit F180, das gestreamte `/v1/completions` mit F186 (upstreams eigener Stream, Wahrscheinlichkeiten eingeschlossen), `n > 1` mit F180 und auf allen nativen Wegen mit F197.
+- **Nachtrag (Branch-Review 2026-10-20):** Die Reproduktionstests heißen seit F186 (`4fb82c1`) anders, weil die Mapper-Tests gemeinsamer Felder seitdem über den Chat-Weg laufen: `#llamaCppSamplingExtensionsReachTheStreamedCompletion` ist `#llamaCppSamplingExtensionsReachTheMappedRequest`, `#jsonSchemaReachesTheStreamedCompletionUnlessAGrammarIsGiven` wurde erst `#jsonSchemaReachesTheMappedRequestUnlessAGrammarIsGiven` und mit F218 in `#jsonSchemaReachesTheMappedRequest` und `#jsonSchemaAndGrammarAreBothForwardedSoUpstreamCanRefuseThePair` geteilt; `#nPredictWinsOverMaxTokensLikeUpstream` ist unverändert.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `1149533`
+
+
+### F96 – java-llama.cpp: Anthropic-Anfrage ohne `max_tokens` läuft unbegrenzt, `chat_template_kwargs` geht verloren
+- **Zeit:** 2026-10-18 05:45 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java:111` (`toOpenAiChatRequest`)
+- **Schwere:** niedrig
+- **Beschreibung:** Aus dem offenen Verdacht „Anthropic-Rest nach F76“. upstreams `convert_anthropic_to_oai` (`server-chat.cpp:585`, b11639) setzt `max_tokens` 4096, wenn die Anfrage keines trägt, und reicht `chat_template_kwargs` durch. `/v1/messages` übernahm `max_tokens` nur, wenn vorhanden, eine Anfrage ohne generierte also, bis der Kontext voll war; und `chat_template_kwargs` (etwa Qwen3s `enable_thinking`) wurde verworfen, obwohl der nachgeschaltete `OpenAiRequestMapper` es seit F77 liest.
+- **Fix:** Ohne Zahl in `max_tokens` wird `DEFAULT_MAX_TOKENS` (4096) gesetzt; ein Objekt in `chat_template_kwargs` wird kopiert.
+- **Reproduktion:** `AnthropicApiSupportTest#requestWithoutMaxTokensIsCappedLikeUpstream`, `#requestPassesChatTemplateKwargsLikeUpstream` (zunächst ein Sammeltest, nach der Regel „ein Test je Richtung“ aufgeteilt)
+- **Rot-Grün-Nachweis:** gegen die Produktionsdatei vor `4b20c57` rot, jeder Teil für sich (2 Fehlschläge: „Expected: is <4096> but: was <0>“, „Expected: is  but: was “ für `chat_template_kwargs`), mit Fix grün (21/21 der Klasse, skipped 0). `OpenAiCompatServerHttpTest` 46/46, `AnthropicStreamTranslatorTest` 5/5, `OpenAiRequestMapperTest` 25/25, `ResponsesApiSupportTest` 18/18, `OllamaApiSupportTest` 24/24, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `4b20c57`
+
+
+### F97 – java-llama.cpp: Ollama-`/api/chat` verliert den Denkinhalt in beide Richtungen
+- **Zeit:** 2026-10-18 06:00 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java:267` (`toOllamaChatResponse`, `toOllamaContentLine`, `toOpenAiMessage`)
+- **Schwere:** niedrig
+- **Beschreibung:** Aus dem offenen Verdacht „Ollama-Antworten tragen den Denkinhalt nicht“. Ollama legt den Denkinhalt eines Modells in `message.thinking` ab, blockierend und je Stream-Zeile, und Clients senden ihn im Verlauf zurück. `OllamaApiSupport` übernahm aus OpenAIs Antwort nur `content` und `tool_calls`; im Stream lieferte `toOllamaContentLine` für einen Chunk ohne Text `null`, Denk-Deltas fielen also ganz weg. Seit F78 kann ein Client mit `think` das Denken steuern, sah es aber nie. Im Verlauf blieb `thinking` als unbekannter Schlüssel stehen, den llama.cpps Parser ignoriert (`common/chat.cpp:422` liest `reasoning_content`), wie bei F84 für Anthropic.
+- **Fix:** Blockierend und im Stream wird `reasoning_content` zu `message.thinking` (eine Denk-Zeile trägt `content` leer); im Verlauf wird `thinking` zu `reasoning_content`.
+- **Reproduktion:** `OllamaApiSupportTest#blockingChatAnswerCarriesTheThinkingLikeOllama`, `#streamedThinkingDeltaBecomesAThinkingLineLikeOllama`, `#historyThinkingBecomesReasoningContent` (zunächst ein Sammeltest, nach der Regel „ein Test je Richtung“ aufgeteilt)
+- **Rot-Grün-Nachweis:** gegen die Produktionsdatei vor `fb4dd64` rot, jede Richtung für sich (3 Fehlschläge: „Expected: is "6 times 7" but: was ""“ blockierend und im Verlauf, „Expected: is "6 ti" but: was ""“ im Stream), mit Fix grün (27/27 der Klasse, skipped 0). `OpenAiCompatServerHttpTest` 46/46, `AnthropicApiSupportTest` 20/20, `ResponsesApiSupportTest` 18/18, `OpenAiRequestMapperTest` 25/25, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `fb4dd64`
+
+
+### F98 – java-llama.cpp: gestreamtes Ollama-`/api/chat` meldet keine Tokenzahlen
+- **Zeit:** 2026-10-18 06:15 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java:545` (`streamOllamaChat`), `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java` (`toOllamaDoneLine`, `usageOf`)
+- **Schwere:** niedrig
+- **Beschreibung:** Aus dem Nachtrag zum offenen Verdacht „`done_reason` uneinheitlich“. Das blockierende `/api/chat` meldet `prompt_eval_count`/`eval_count`, die Abschlusszeile des Streams nicht. `streamOllamaChat` forderte den Usage-Chunk auch nicht an (anders als die Anthropic- und Responses-Routen mit `withUsageChunk`), upstream sendet ihn nur bei `stream_options.include_usage`. Clients, die Token/s aus `eval_count` der Abschlusszeile berechnen (Ollamas eigene CLI mit `--verbose`, Open WebUI), sahen nichts. Gefunden über den Vergleich blockierender und gestreamter Pfad (Erkenntnis aus F92).
+- **Fix:** `streamOllamaChat` fordert den Usage-Chunk an (`withUsageChunk`), merkt sich ihn über `OllamaApiSupport.usageOf` und gibt ihn an `toOllamaDoneLine(model, accumulator, usage)` weiter, das die Zählung wie die blockierende Antwort einträgt. `done_reason` bleibt unverändert (Teil des Verdachts, Entscheidung des Nutzers).
+- **Reproduktion:** `OpenAiCompatServerHttpTest#ollamaChatStreamingDoneLineCarriesTheTokenCountsLikeTheBlockingAnswer` (das `FakeBackend` sendet den Usage-Chunk wie upstream nur bei `include_usage`)
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is <12> but: was <-1>“), mit Fix grün (47/47 der Klasse, skipped 0); Gegenprobe per `git stash` beider Produktionsdateien rot (1), nach `pop` grün. `OllamaApiSupportTest` 27/27, `AnthropicApiSupportTest` 21/21, `ResponsesApiSupportTest` 18/18, `OpenAiRequestMapperTest` 25/25, `AnthropicStreamTranslatorTest` 5/5, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `95af953`
+
+
+### F99 – java-llama.cpp: blockierende Responses-Antwort trägt ein leeres `message`-Item neben Werkzeugaufrufen
+- **Zeit:** 2026-10-18 06:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:378` (`toResponsesResponse`)
+- **Schwere:** niedrig
+- **Beschreibung:** Vergleich blockierender und gestreamter Pfad (Erkenntnis aus F92). `toResponsesResponse` legte immer ein `message`-Item an, auch mit leerem Text; bei einer Runde, die nur Werkzeuge aufruft, stand so ein leeres `output_text` vor den `function_call`-Items. upstreams `to_json_oaicompat_resp` (`server-task.cpp`, b11639) legt das Item nur bei nicht leerem Text an, und unser eigener Stream (`ResponsesStreamTranslator`) öffnet es erst mit dem ersten Text-Delta. Dieselbe Anfrage ergab also blockierend eine andere `output`-Liste als gestreamt; ein Client, der das leere Item zurückschickt, erzeugt im nächsten Zug eine leere Assistenten-Nachricht im Verlauf. Der Test `responseEmitsFunctionCallItemsForToolCalls` schrieb das leere Item fest („output[0] is the (empty) message“).
+- **Fix:** Das `message`-Item wird nur bei nicht leerem Text angelegt; der festschreibende Test erwartet den `function_call` jetzt an Stelle 0.
+- **Reproduktion:** `ResponsesApiSupportTest#toolCallOnlyResponseHasNoEmptyMessageItemLikeUpstreamAndTheStream`, dazu `#responseEmitsFunctionCallItemsForToolCalls`
+- **Rot-Grün-Nachweis:** ohne Fix rot (2 Fehlschläge: „Expected: is <1> but: was <2>“, „Expected: is "function_call" but: was "message"“), mit Fix grün (19/19 der Klasse, skipped 0); Gegenprobe per `git stash` rot (2), nach `pop` grün. `ResponsesStreamTranslatorTest` 5/5, `OpenAiCompatServerHttpTest` 47/47, `AnthropicApiSupportTest` 21/21, `OllamaApiSupportTest` 27/27, `OpenAiRequestMapperTest` 25/25, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `1939435`
+
+
+### F102 – java-llama.cpp: Chat-Anfrage mit `id_slot: -1` wird mit 400 abgewiesen
+- **Zeit:** 2026-10-18 07:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java:133` (`applyCommonFields`)
+- **Schwere:** niedrig
+- **Beschreibung:** upstream liest `id_slot` mit dem Standardwert `-1` („beliebiger Slot“, `server-context.cpp:4845`, b11639). Der Mapper reichte jeden ganzzahligen Wert an `InferenceParameters.withSlotId` weiter, das negative Werte mit `IllegalArgumentException` ablehnt; der Server macht daraus 400. Ein Client, der den llama.cpp-Standard ausdrücklich mitschickt, scheiterte so auf beiden Chat-Pfaden und beim gestreamten `/v1/completions`, während das nicht gestreamte `/v1/completions` (wörtlich an upstream durchgereicht) dieselbe Anfrage annahm.
+- **Fix:** Ein negativer `id_slot` wird nicht gesetzt, das entspricht upstreams Standard. `withSlotId` bleibt streng (ein Java-Aufrufer pinnt bewusst einen Slot).
+- **Reproduktion:** `OpenAiRequestMapperTest#slotMinusOneMeansAnySlotLikeUpstream`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Unexpected exception thrown: java.lang.IllegalArgumentException: slotId must be non-negative but was -1“, als Assertion über `assertDoesNotThrow`), mit Fix grün (26/26 der Klasse, skipped 0); Gegenprobe per `git stash` rot (1), nach `pop` grün. `OpenAiCompatServerHttpTest` 47/47, `AnthropicApiSupportTest` 21/21, `ResponsesApiSupportTest` 19/19, `OllamaApiSupportTest` 27/27, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `57cbdc0`
+
+
+### F103 – java-llama.cpp: `max_tokens: 0` generiert unbegrenzt statt nur den Cache zu füllen
+- **Zeit:** 2026-10-18 07:35 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java:164` (`applyCommonFields`)
+- **Schwere:** niedrig
+- **Beschreibung:** Musterdurchsicht nach F102 (Werte, die der Mapper anders behandelt als upstream). upstreams Schema (`server-schema.cpp`, `n_predict` mit den Aliasen `max_completion_tokens`/`max_tokens`, Grenzen -1..INT32_MAX, b11639): „When 0, no tokens will be generated but the prompt is evaluated into the cache“. Der Mapper übernahm die Obergrenze nur bei `> 0`; ein Client, der mit `0` den Cache vorwärmt, bekam auf den gemappten Wegen (beide Chat-Pfade, gestreamtes `/v1/completions`, und damit Anthropic/Responses/Ollama) eine Generierung ohne Obergrenze bis zum Kontextende, das wörtlich durchgereichte `/v1/completions` dagegen keine.
+- **Fix:** Die Obergrenze wird bei `>= 0` gesetzt; `-1` bzw. kein Feld lässt upstreams Standard (unbegrenzt). (**Ergänzt in F219:** jeder Wert außer -1 wird durchgereicht, damit upstream -2 und darunter mit 400 abweist.)
+- **Reproduktion:** `OpenAiRequestMapperTest#zeroTokenCapIsForwardedAsUpstreamReadsIt` (bis 2026-10-21 `zeroTokenCapOnlyWarmsTheCacheLikeUpstream`)
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is <0> but: was <-1>“), mit Fix grün (27/27 der Klasse, skipped 0); Gegenprobe per `git stash` rot (1), nach `pop` grün. `OpenAiCompatServerHttpTest` 47/47, `AnthropicApiSupportTest` 21/21, `ResponsesApiSupportTest` 19/19, `OllamaApiSupportTest` 27/27, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `d7f91db`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J2-2):** Titel und Beschreibung beschreiben eine Wirkung, die der Server nicht hat: `max_tokens: 0` füllt nicht nur den Cache, sondern erzeugt **ein** Token (Messung J2, F158; `withNPredict` sagt es seit dem Branch). Upstreams README-Satz („When 0, no tokens will be generated but the prompt is evaluated into the cache“) stammt aus PR #1937, dessen Zweig `n_predict == 0` → EOS es bei b11639 nicht mehr gibt; `server-context.cpp` übernimmt 0 unverändert in `n_predict_max`, und die einzige Budgetprüfung (`slot.stats.n_gen > 0 && … && !slot.has_budget()`) läuft erst nach dem ersten Token. Der Fix bleibt richtig (0 wird wie auf dem wörtlich durchgereichten `/v1/completions` weitergegeben statt „unbegrenzt“ zu bedeuten); der Mapper-Kommentar nennt das eine Token jetzt, der Test heißt `zeroTokenCapIsForwardedAsUpstreamReadsIt` mit unveränderter Assertion (`n_predict` ist 0). Reine Doku, kein neuer Test.
+
+
+### F104 – java-llama.cpp: Ollama-Routen verwerfen `repeat_penalty`, `min_p` und weitere Optionen; `num_predict: -2` wird abgewiesen
+- **Zeit:** 2026-10-18 07:50 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java:246` (`copyLlamaCppOptions`, `copyNumPredict`, neu; Aufrufe in `toOpenAiChatRequest`, `toOpenAiCompletionRequest`, `toInfillRequest`)
+- **Schwere:** niedrig
+- **Beschreibung:** Musterdurchsicht nach F102/F103 (Sonderwerte) und F74 (Optionsmengen der Geschwister). Ollama legt die Sampling-Optionen unter `options` mit llama.cpps Namen ab (`repeat_penalty`, `repeat_last_n`, `min_p`, `typical_p`, `presence_penalty`, `frequency_penalty`, `num_keep`), Clients wie Open WebUI setzen sie. Alle drei Ollama-Routen übernahmen nur `temperature`, `top_p`, `top_k`, `seed`, `num_predict` und `stop`; der Rest ging still verloren, obwohl jede Route ihn an upstreams Schema weitergeben kann (`/api/chat` seit F95 über den Mapper, `/api/generate` und FIM wörtlich). Dazu ein Sonderwert: Ollamas `num_predict: -2` („Kontext füllen“) ging unverändert als `max_tokens`/`n_predict` -2 weiter, upstreams harte Grenze beginnt bei -1 – `/api/generate` und FIM antworteten mit einem Fehler.
+- **Fix:** Ein gemeinsamer Helfer kopiert die llama.cpp-benannten Optionen auf allen drei Routen (`num_keep` → `n_keep`); `repeat_last_n: -1` (Ollama: Kontextgröße, upstream: ungültig) bleibt beim Server-Standard; `num_predict` unter -1 wird -1.
+- **Reproduktion:** `OllamaApiSupportTest#chatCopiesTheLlamaCppSamplingOptions`, `#generateCopiesTheLlamaCppSamplingOptions`, `#infillCopiesTheLlamaCppSamplingOptions`, `#numPredictMinusTwoFillsTheContextAsUpstreamsMinusOne`; `#repeatLastNMinusOneIsLeftToTheServerDefault` schützt die Ausnahme (ohne Fix grün, weil gar nichts kopiert wurde)
+- **Rot-Grün-Nachweis:** ohne Fix rot (4 Fehlschläge: dreimal „Expected: is <0.05> but: was <0.0>“, einmal „Expected: is <-1> but: was <-2>“), mit Fix grün (32/32 der Klasse, skipped 0); Gegenprobe per `git stash` rot (4), nach `pop` grün. `OpenAiCompatServerHttpTest` 47/47, `AnthropicApiSupportTest` 21/21, `ResponsesApiSupportTest` 19/19, `OpenAiRequestMapperTest` 27/27, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `ce983cf`
+
+
+### F105 – java-llama.cpp: Responses-Anfrage mit `previous_response_id` wird ohne Verlauf beantwortet
+- **Zeit:** 2026-10-18 08:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:56` (`toOpenAiChatRequest`)
+- **Schwere:** mittel
+- **Beschreibung:** Gefunden beim Lesen von upstreams `server_chat_convert_responses_to_chatcmpl` (`server-chat.cpp`, b11639) von oben (Regel aus F90): Gleich nach der `input`-Prüfung lehnt upstream `previous_response_id` mit „llama.cpp does not support 'previous_response_id'.“ ab. `/v1/responses` hier ignorierte das Feld. Es gibt keinen Antwortspeicher, also beantwortete der Server einen Zug, der nur über die ID an den vorigen anknüpft (OpenAIs Standardweg mit `store`), ohne dessen Verlauf – still eine Antwort auf eine andere Frage.
+- **Fix:** Ein nicht leeres `previous_response_id` wirft eine `IllegalArgumentException` (seit F89 eine 400) mit upstreams Wortlaut, dem Wert und dem Hinweis, die früheren Züge in `input` zu senden.
+- **Reproduktion:** `ResponsesApiSupportTest#previousResponseIdIsRejectedLikeUpstream`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“), mit Fix grün (20/20 der Klasse, skipped 0); Gegenprobe per `git stash` rot (1), nach `pop` grün. `ResponsesStreamTranslatorTest` 5/5, `OpenAiCompatServerHttpTest` 47/47, `AnthropicApiSupportTest` 21/21, `OllamaApiSupportTest` 32/32, `OpenAiRequestMapperTest` 27/27, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `f9e680a`
+
+
+### F106 – java-llama.cpp: Responses-Anfrage verliert `top_k`, `seed`, `stop` und llama.cpps Erweiterungen
+- **Zeit:** 2026-10-18 08:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:146` (`toOpenAiChatRequest`, `CHAT_PASSTHROUGH_FIELDS` neu)
+- **Schwere:** niedrig
+- **Beschreibung:** Aus dem offenen Verdacht der vorigen Runde. upstreams `convert_responses_to_chatcmpl` (`server-chat.cpp`, b11639) beginnt mit `chatcmpl_body = response_body` und kopiert damit jedes Feld in die Chat-Anfrage, die der Chat-Parser liest. `ResponsesApiSupport` übernahm nur `temperature`, `top_p`, `max_output_tokens` und `reasoning.effort`; `top_k`, `seed`, `stop`, die Strafen, `chat_template_kwargs`, `response_format`, die Cache-Felder und llama.cpps Sampling-Erweiterungen gingen still verloren, obwohl der nachgeschaltete Mapper sie seit F95 liest (Muster F74/F104).
+- **Fix:** Eine Liste der Felder, die der Chat-Mapper liest, wird unverändert übernommen; `input`, `tools` und `tool_choice` bleiben übersetzt statt kopiert.
+- **Reproduktion:** `ResponsesApiSupportTest#requestFieldsTheChatMapperReadsPassThroughLikeUpstream` (parametrisiert, ein Fall je Feld, 27 Fälle)
+- **Rot-Grün-Nachweis:** ohne Fix rot, jeder Fall für sich (27 Fehlschläge, z. B. „Expected: is <0.05> but: was <>“), mit Fix grün (47/47 der Klasse, skipped 0); Gegenprobe per `git stash` rot (27), nach `pop` grün. `ResponsesStreamTranslatorTest` 5/5, `OpenAiCompatServerHttpTest` 47/47, `AnthropicApiSupportTest` 21/21, `OllamaApiSupportTest` 32/32, `OpenAiRequestMapperTest` 27/27, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `b370643`
+
+
+### F107 – java-llama.cpp: Anthropic-`tool_choice` `none` läuft als `auto`
+- **Zeit:** 2026-10-18 08:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java:348` (`toOpenAiToolChoice`)
+- **Schwere:** niedrig
+- **Beschreibung:** Anthropics `tool_choice` `{"type":"none"}` verbietet Werkzeugaufrufe (Clients setzen es, um eine Runde ohne Werkzeug zu erzwingen, etwa für eine Zusammenfassung, während die Werkzeugliste für den Prompt-Cache gleich bleibt). `toOpenAiToolChoice` kannte nur `auto`, `any` und `tool`; `none` ergab kein `tool_choice`, die Anfrage lief als `auto`, und das Modell durfte die mitgeschickten Werkzeuge aufrufen. upstreams `convert_anthropic_to_oai` (b11639) hat dieselbe Lücke; der Chat-Mapper und llama.cpps Chat-Parser verstehen OpenAIs `tool_choice` `none` aber, die Übersetzung kostet eine Zeile. Hier also bewusst über upstream hinaus.
+- **Fix:** `none` wird zu `tool_choice` `none`.
+- **Reproduktion:** `AnthropicApiSupportTest#toolChoiceNoneForbidsToolCalls`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "none" but: was ""“), mit Fix grün (22/22 der Klasse, skipped 0); Gegenprobe per `git stash` rot (1), nach `pop` grün. `AnthropicStreamTranslatorTest` 5/5, `OpenAiCompatServerHttpTest` 47/47, `ResponsesApiSupportTest` 47/47, `OllamaApiSupportTest` 32/32, `OpenAiRequestMapperTest` 27/27, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `d58662f`
+
+
+### F108 – java-llama.cpp: Responses-`text.format` (strukturierte Ausgabe) wird ignoriert
+- **Zeit:** 2026-10-18 08:45 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java:177` (`appendResponseFormat`, neu; Aufruf in `toOpenAiChatRequest`)
+- **Schwere:** niedrig
+- **Beschreibung:** Die Responses-API trägt strukturierte Ausgabe in `text.format` (`{"type":"json_schema","name","schema","strict"}` bzw. `{"type":"json_object"}`), die Chat-API in `response_format` mit dem Schema unter `json_schema`. `/v1/responses` übersetzte das nicht; ein Client, der ein Schema verlangte, bekam freien Text ohne Grammatik. upstreams `convert_responses_to_chatcmpl` (b11639) kopiert `text` unverändert in die Chat-Anfrage, wo der Chat-Parser es nicht liest – dieselbe Lücke. Der Chat-Mapper reicht `response_format` an die native Seite weiter, die Übersetzung reicht also.
+- **Fix:** `json_schema` wird zu `response_format` mit `name`, `description`, `schema` und `strict` unter `json_schema`; `json_object` wird übernommen; `text` braucht nichts. Ein vom Client zusätzlich gesetztes `response_format` (seit F106 durchgereicht) hat Vorrang.
+- **Reproduktion:** `ResponsesApiSupportTest#textFormatJsonSchemaBecomesTheChatResponseFormat`, `#textFormatJsonObjectBecomesTheChatResponseFormat`; `#textFormatTextLeavesTheResponseFormatOut` schützt den `text`-Fall (ohne Fix grün)
+- **Rot-Grün-Nachweis:** ohne Fix rot (2 Fehlschläge: „Expected: is "json_schema" but: was ""“, „Expected: is "json_object" but: was ""“), mit Fix grün (50/50 der Klasse, skipped 0); Gegenprobe per `git stash` rot (2), nach `pop` grün. `ResponsesStreamTranslatorTest` 5/5, `OpenAiCompatServerHttpTest` 47/47, `AnthropicApiSupportTest` 22/22, `OllamaApiSupportTest` 32/32, `OpenAiRequestMapperTest` 27/27, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün (eine erste Fassung mit einem Array-Literal meldete `SACM_STATIC_ARRAY_CREATED_IN_METHOD`, jetzt eine Konstante).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `8e91168`
+
+
+### F109 – java-llama.cpp: `tool_choice`, das eine Funktion benennt, läuft als `auto`
+- **Zeit:** 2026-10-18 09:00 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java:74`, `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java` (`toOpenAiChatRequest`, Werkzeugblock)
+- **Schwere:** niedrig
+- **Beschreibung:** Musterdurchsicht nach F107. OpenAI-Chat benennt ein Werkzeug mit `tool_choice` `{"type":"function","function":{"name":"f"}}`, die Responses-API mit `{"type":"function","name":"f"}`. Mapper und Responses-Übersetzung nahmen nur die Textform; ein Objekt ging verloren, die Anfrage lief als `auto`, das Modell durfte antworten, ohne das verlangte Werkzeug aufzurufen. upstream liest `tool_choice` nur als Zeichenkette (`json_value(body, "tool_choice", "auto")`, `server-common.cpp:1282`, b11639; `common_chat_tool_choice_parse_oaicompat` kennt `auto`/`none`/`required`) und fällt bei einem Objekt ebenso auf `auto` zurück. Die Anthropic-Übersetzung bildet ihr `{"type":"tool"}` schon auf `required` ab.
+- **Fix:** Ein Objekt mit `type` `function` wird auf beiden Wegen zu `required`, dem nächsten Wert, den llama.cpp kennt (mit einem einzigen Werkzeug genau das verlangte).
+- **Reproduktion:** `OpenAiRequestMapperTest#toolChoiceNamingAFunctionRequiresAToolCall`, `ResponsesApiSupportTest#toolChoiceNamingAFunctionRequiresAToolCall`
+- **Rot-Grün-Nachweis:** ohne Fix rot, je Weg für sich (2 Fehlschläge, je „Expected: is "required" but: was ""“), mit Fix grün (28/28 und 51/51, skipped 0); Gegenprobe per `git stash` beider Produktionsdateien rot (1 + 1), nach `pop` grün. `OpenAiCompatServerHttpTest` 47/47, `AnthropicApiSupportTest` 22/22, `ResponsesStreamTranslatorTest` 5/5, `OllamaApiSupportTest` 32/32, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Nachtrag (2026-10-18, nur beschrieben):** Zwei Kommentare neben dem Fix sind nicht nachgezogen: die Klassen-Javadoc von `OpenAiRequestMapper` nennt `tool_choice` unter den Feldern, die „verbatim as raw JSON“ weitergehen (galt schon vorher nur für die Textform), und der Kommentar über dem Werkzeugblock in `ResponsesApiSupport.toOpenAiChatRequest` beschreibt nur die Textform. Reine Doku-Abweichung, nach der Nutzerregel ohne Test nicht nachträglich geändert; Vorschlag: `tool_choice` aus der Verbatim-Aufzählung nehmen und „ein benanntes Werkzeug wird zu `required`“ ergänzen.
+- **Nachtrag (Branch-Review 2026-10-20, Funde J2-6, J5-8):** Erledigt. Die Klassen-Javadoc von `OpenAiRequestMapper` nennt als wörtlich weitergereicht nur noch `messages` und `tools`; `tool_choice` geht in der Textform weiter, ein benanntes Werkzeug wird zu `required` mit nur diesem Werkzeug (F227), jede andere Objektform (`allowed_tools`, `custom`) wird ignoriert und läuft als `auto`, `parallel_tool_calls` wird als Boolean gelesen. Der Kommentar über `tool_choice` in `ResponsesApiSupport.toOpenAiChatRequest` sagt dasselbe. Reine Doku, kein Test.
+- **Nachtrag (Branch-Review 2026-10-20):** Mit mehreren Werkzeugen ließ `required` jeden Aufruf zu; seit F227 geht auf beiden Wegen nur das benannte Werkzeug weiter, ein unbekannter oder fehlender Name ist 400.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `c5823a5`
+
+
+### F111 – java-llama.cpp: Rerank im TEI-Format (`texts`) wird mit 400 abgewiesen
+- **Zeit:** 2026-10-18 09:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OaiRerankSupport.java:113` (`readDocuments`, `toResponse`/`toTeiResponse` neu), `llama/src/main/java/net/ladenthin/llama/server/LlamaModelBackend.java` (`rerank`)
+- **Schwere:** niedrig
+- **Beschreibung:** upstreams `/rerank` (`server-context.cpp:5705`, b11639) unterstützt neben dem Jina/Cohere-Format auch Hugging Faces text-embeddings-inference: eine Anfrage mit `texts` statt `documents` wird angenommen und als bloßes Array `[{index, score}]` beantwortet, bestes zuerst, mit `text` bei `return_text` (`format_response_rerank`, `server-common.cpp:1535`). Der Server hier registriert `/rerank` ebenfalls, verlangte aber `documents` und antwortete TEI-Clients (etwa LangChains `HuggingFaceTEI`-Reranker oder Werkzeuge, die TEI als Reranker ansprechen) mit 400.
+- **Fix:** `readDocuments` liest `texts`, wenn `documents` fehlt; `toResponse` wählt das Antwortformat nach der Anfrage, `toTeiResponse` liefert das TEI-Array (Text aus dem `document` der nativen Antwort). Die Naht `toResponse` wurde zuerst verhaltensneutral eingezogen (delegiert an `toOaiResponse`), das Rot ist gegen diesen Stand gezeigt.
+- **Reproduktion:** `OaiRerankSupportTest#readDocumentsAcceptsTheTeiTexts`, `#teiRequestGetsTheTeiArrayWithScoreLikeUpstream`, `#teiReturnTextAddsTheDocumentText`
+- **Rot-Grün-Nachweis:** gegen den Stand mit der neutralen Naht rot, jeder Teil für sich (3 Fehlschläge: „Unexpected exception thrown: … 'documents' must be a non-empty array“, „Expected: is  but: was “, „Expected: is "b" but: was ""“), mit Fix grün (13/13 der Klasse, skipped 0). Die `git stash`-Gegenprobe ging hier nicht (ohne Naht kompiliert der Test nicht; der danach gelesene Bericht war der alte) und wurde durch den Lauf gegen den Naht-Stand ersetzt. `OpenAiCompatServerHttpTest` 47/47, `AnthropicApiSupportTest` 22/22, `ResponsesApiSupportTest` 51/51, `OllamaApiSupportTest` 32/32, `OpenAiRequestMapperTest` 28/28, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `6ff8eea`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J6-2):** `toResponse` hatte drei Zweige, die kein Test sah: kein Test schickte eine `documents`-Anfrage durch `toResponse` (nur der modellgestützte `OpenAiServerRerankIntegrationTest`, der sich lokal ohne Reranker überspringt), keine TEI-Anfrage setzte `top_n`, und keine prüfte, dass `text` ohne `return_text` fehlt. `OaiRerankSupportTest#documentsRequestGetsTheOpenAiObjectThroughToResponse` pinnt die OpenAI-Form samt Modell-Id, `#teiRequestHonoursTopNAndOmitsTextByDefault` die TEI-Form mit `top_n: 1` (nur der beste Eintrag) und ohne `text` (upstreams `format_response_rerank`, b11639: `resize(min(top_n, n))` vor der TEI-Rückkehr, `return_text` standardmäßig `false`; das `top_n` betrifft auch F177). Gegenprobe rot: Bedingung `true` („Expected: is  but: was “ auf `isObject()`), `readTopN(request)` → `-1` im TEI-Zweig („Expected: is <1> but: was <2>“), `asBoolean(false)` → `asBoolean(true)` („Expected: is  but: was “); unverändert 24/24 der Klasse grün.
+- **Nachtrag (Branch-Review 2026-10-20, Fund J6-3):** Die TEI-Form über die echte Route: `OpenAiServerRerankIntegrationTest#teiRequestIsAnsweredAsABareArray` schickt `texts` mit `return_text: true` und prüft das bloße Array mit `index`/`score` (kein `relevance_score`) und dass `text` das zum `index` gehörige gesendete Dokument ist. Lokal mit dem Winzmodell als Reranker grün; mit nicht erkanntem `texts` rot („Expected: is  but: was “ auf `isArray()`).
+- **Nachtrag (Branch-Review 2026-10-20, Fund J6-6):** Die README-Routentabelle, die Klassen-Javadoc von `OaiRerankSupport`, die Javadoc von `OpenAiBackend.rerank` und die `server`-Zeile in `CLAUDE.md` beschrieben `/v1/rerank` nur mit `results`/`data`. Seit Commit `c5e53cc` nennen alle vier die TEI-Anfrage (`texts`, `return_text`), das bloße Array `[{index, score[, text]}]`, die Wahl der Form nach `texts` wie upstreams `format_response_rerank` und das `usage`-Objekt der OpenAI-Form (F177). Reine Doku, kein Test.
+
+
+### F112 – java-llama.cpp: Unterpfade einer Route laufen durch deren Handler (`/v1/messages/count_tokens` erzeugt eine ganze Antwort)
+- **Zeit:** 2026-10-18 09:50 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java:271` (`register`, `exactPath` neu)
+- **Schwere:** mittel
+- **Beschreibung:** Gefunden beim Abgleich mit upstreams Routenliste (`tools/server/server.cpp`, b11639). `com.sun.net.httpserver` ordnet eine Anfrage dem Kontext mit dem **längsten Präfix** zu, nicht dem exakten Pfad, und kein Handler prüfte den Pfad nach. Damit beantwortete jede Route auch alle Pfade, die mit ihr beginnen: `POST /v1/messages/count_tokens` (Claude Code zählt dort Token vor dem Senden) lief als vollständige Nachricht – eine Generierung bis zu 4096 Token (F96) statt eines Fehlers; ebenso upstreams `/v1/chat/completions/input_tokens` und `/v1/responses/input_tokens`, und `/v1/modelsXYZ` lieferte die Modellliste. upstream ordnet exakt zu.
+- **Fix:** `register` umhüllt jeden Handler außer dem Sammel-Kontext `/` mit `exactPath`: nur der Pfad selbst (und mit einem abschließenden Schrägstrich) erreicht den Handler, jeder andere bekommt 404. `/v1/reranking`, das nur über das Präfix von `/v1/rerank` erreichbar war, ist jetzt eigens registriert wie bei upstream.
+- **Reproduktion:** `OpenAiCompatServerHttpTest#aSubPathOfARouteIsNotHandledByTheRoute`; `#upstreamsRerankingAliasStillAnswers` schützt den Alias und den abschließenden Schrägstrich (ohne Fix grün)
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is <404> but: was <200>“), mit Fix grün (49/49 der Klasse, skipped 0); Gegenprobe per `git stash` rot (1, Bericht vor dem Lauf gelöscht), nach `pop` grün. Alle 25 Testklassen des Pakets `server` grün, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün. Der Agent (`llama-atmosphere-agent`) ruft keine Unterpfade auf.
+- **Offen (nicht geändert):** upstream bietet `/v1/messages/count_tokens` und die `input_tokens`-Routen tatsächlich an; sie hier nachzubauen bräuchte eine Tokenzählung über das Backend (`OpenAiBackend` hat keine) – ein API-Zusatz.
+- **Nachtrag (Branch-Review 2026-10-21, Abschluss vor 6.0.0):** Die Zählrouten upstreams sind mit F261 bedient (`/v1/messages/count_tokens`, `/v1/responses/input_tokens`, `/v1/chat/completions/input_tokens`); `#aSubPathOfARouteIsNotHandledByTheRoute` prüft das Präfix-Matching seitdem an `/v1/messages/batches` und `/v1/chat/completions/other`.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `909edc5`
+
+
+### F113 – java-llama.cpp: `/v1/models` lässt das Pflichtfeld `created` weg
+- **Zeit:** 2026-10-18 10:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiSseFormatter.java:142` (`modelsJson`)
+- **Schwere:** niedrig
+- **Beschreibung:** Das OpenAI-Modellobjekt hat vier Pflichtfelder (`id`, `object`, `created`, `owned_by`); `modelsJson` setzte `created` nicht. Streng typisierte Clients lesen die Liste dann nicht ein (etwa Rusts `async-openai`, dessen `Model` `created: u32` verlangt, oder Clients, die ihre Modelle mit dem OpenAI-Schema validieren). upstreams `get_res_model_info` (`server-context.cpp:5078`, b11639) setzt `created` auf die aktuelle Unix-Zeit.
+- **Fix:** `created` wird mit der aktuellen Unix-Zeit in Sekunden gesetzt, wie upstream.
+- **Reproduktion:** `OpenAiSseFormatterTest#modelsJsonCarriesTheRequiredCreatedTimestampLikeUpstream`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is  but: was “ für `isIntegralNumber`), mit Fix grün (15/15 der Klasse, skipped 0); Gegenprobe per `git stash` rot (1), nach `pop` grün. `OpenAiCompatServerHttpTest` 49/49, `OaiRerankSupportTest` 13/13, `AnthropicApiSupportTest` 22/22, `ResponsesApiSupportTest` 51/51, `OllamaApiSupportTest` 32/32, `OpenAiRequestMapperTest` 28/28, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `b313ad2`
+
+
+### F114 – java-llama.cpp: `/api/version` meldet 0.1.0, VS Code Copilots Ollama-Anbieter lehnt den Server ab
+- **Zeit:** 2026-10-18 10:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java:36` (`OLLAMA_VERSION`)
+- **Schwere:** mittel
+- **Beschreibung:** Die Ollama-Routen sind laut CLAUDE.md und Klassen-Javadoc für Copilots Ollama-Anbieter gedacht (`/api/show` meldet dafür Fähigkeiten und Kontextlänge). Dieser Anbieter (`microsoft/vscode-copilot-chat`, `src/extension/byok/vscode-node/ollamaProvider.ts`) prüft vor jeder Modelloperation `GET /api/version` gegen `MINIMUM_OLLAMA_VERSION = '0.6.4'` (Teile numerisch verglichen, nur `0.0.0` gilt als Entwicklerversion) und bricht sonst mit „Ollama server version … is not supported“ ab. Der Server meldete `0.1.0` – der ganze Copilot-Weg scheiterte an dieser Prüfung. Die Javadoc der Konstante behauptete, Clients prüften nur, dass eine Version kommt.
+- **Fix:** `OLLAMA_VERSION` ist `0.9.0`, die erste Ollama-Version mit `think` (die neueste Anfragefunktion, die die Routen seit F78/F97 übersetzen); die Javadoc nennt jetzt den Grund.
+- **Reproduktion:** `OllamaApiSupportTest#versionPassesCopilotsMinimumOllamaVersion` (Copilots Vergleich nachgebildet)
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is  but: was “), mit Fix grün (33/33 der Klasse, skipped 0); Gegenprobe per `git stash` rot (1), nach `pop` grün. `OpenAiCompatServerHttpTest` 49/49, `OpenAiSseFormatterTest` 15/15, `OaiRerankSupportTest` 13/13, `AnthropicApiSupportTest` 22/22, `ResponsesApiSupportTest` 51/51, `OpenAiRequestMapperTest` 28/28, `LlamaArchitectureTest` 13/13, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `03ad149`
+
+
+### F115 – java-llama.cpp: `/api/show` verschweigt Continue die Kontextlänge
+- **Zeit:** 2026-10-18 10:35 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java:89` (`showJson`)
+- **Schwere:** niedrig
+- **Beschreibung:** Fortsetzung von F114 (Quelltext der genannten Clients lesen). Continues Ollama-Anbieter (`continuedev/continue`, `core/llm/llms/Ollama.ts`) liest die Kontextlänge eines Modells ausschließlich aus dem Text `parameters` von `/api/show` (Zeilen ` `, Ollamas Modelfile-Form, `num_ctx`), nicht aus `model_info`. `showJson` lieferte `parameters: ""`; Continue rechnete daher mit seiner eigenen Vorgabe statt mit dem Fenster des Servers – bei einem kleineren `-c` schickt es zu lange Prompts, bei einem größeren nutzt es das Fenster nicht.
+- **Fix:** `parameters` trägt `num_ctx ` (dieselbe Zahl wie `model_info["llama.context_length"]`).
+- **Reproduktion:** `OllamaApiSupportTest#showParametersCarryTheContextLengthWhereContinueReadsIt` (mit Continues regulärem Ausdruck)
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "4096" but: was null“), mit Fix grün (34/34 der Klasse, skipped 0); Gegenprobe per `git stash` rot (1), nach `pop` grün. Breite Liste wie F114 grün, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `3e2f3b5`
+
+
+### F116 – java-llama.cpp: Ollama-Werkzeugergebnisse verlieren den Werkzeugnamen
+- **Zeit:** 2026-10-18 10:45 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java:208` (`toOpenAiMessage`)
+- **Schwere:** niedrig
+- **Beschreibung:** Ollama-Clients (etwa Continues Ollama-Anbieter) schicken Werkzeugergebnisse als `{"role":"tool","content":…,"tool_name":…}`, ohne `tool_call_id`. `toOpenAiMessage` kopierte die Nachricht unverändert; llama.cpps Chat-Parser liest den Namen eines Werkzeugergebnisses aber aus `name` (`common/chat.cpp:426`, b11639) und kennt `tool_name` nicht. Ohne Kennung und ohne Namen konnte die Vorlage nicht sagen, welches Werkzeug geantwortet hat – bei mehreren Aufrufen in einer Runde ordnet das Modell die Ergebnisse nur noch nach der Reihenfolge zu.
+- **Fix:** `tool_name` wird zu `name` (ein vorhandenes `name` hat Vorrang).
+- **Reproduktion:** `OllamaApiSupportTest#toolResultNameBecomesTheNameLlamaCppReads`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: is "get_weather" but: was ""“), mit Fix grün (35/35 der Klasse, skipped 0); Gegenprobe per `git stash` rot (1), nach `pop` grün. Breite Liste wie F114 grün, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `e4f54a6`
+
+
+### F117 – java-llama.cpp: Gestreamte Responses-Fehler erreichen Responses-Clients nicht
+- **Zeit:** 2026-10-18 11:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java:782` (`streamResponses`), `ResponsesStreamTranslator.java`
+- **Schwere:** mittel
+- **Beschreibung:** Scheiterte ein gestreamtes `/v1/responses` (abgelehnte Anfrage, Fehler bei der Inferenz), schrieb der Server nach `response.created` nur `event: error` mit `{"error":{…}}` – ohne `type` im Datenobjekt und ohne `response.failed`. Responses-Clients verteilen nach dem `type` der Daten: Codex (`codex-api/src/sse/responses.rs`, `process_responses_event`) verwirft ein Ereignis ohne `type` beim Parsen und liest die Fehlermeldung nur aus `response.failed` (`responses_error.rs`, `parse_failed_response` → `response.error.message`/`code`); Ergebnis war „stream closed before response.completed“ statt der eigentlichen Meldung, etwa des Kontextüberlaufs.
+- **Fix:** `ResponsesStreamTranslator.failed(message, code)` erzeugt ein `response.failed` mit der Antwort (`status: failed`, `error.code`/`error.message`) und fortlaufender `sequence_number`; `streamResponses` schreibt es in beiden Fehlerzweigen vor das bisherige `error`-Ereignis, das für Clients bleibt, die ein `error`-Objekt auf oberster Ebene auswerten (OpenAIs Python-SDK) und upstreams Form entspricht.
+- **Reproduktion:** `OpenAiCompatServerHttpTest#rejectedStreamedResponseEndsWithAResponseFailedEvent`, `#failedStreamedResponseEndsWithAResponseFailedEvent`
+- **Rot-Grün-Nachweis:** ohne Fix beide rot („Expected: a string containing "event: response.failed" but: was "event: response.created …"“), mit Fix grün (51/51 der Klasse, skipped 0); Gegenprobe per `git stash` beider Produktionsdateien rot (2), nach `pop` grün. Breite Liste (Server-Tests inkl. `ResponsesStreamTranslatorTest`, `LlamaArchitectureTest`) grün, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `bd3db6a`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J5-6):** Die laufende `sequence_number` des `response.failed` war nur über den Server und nur per Teilstring geprüft. `ResponsesStreamTranslatorTest#failedContinuesTheSequenceNumber` (begin, ein Textchunk, `failed("boom", "server_error")`) verlangt fünf Ereignisse, das letzte `response.failed` mit `sequence_number` 4, `status` `failed` und `error.code`/`error.message`. Gegenprobe mit `sequence = 0` in `failed()` rot („Expected: is <4> but: was <0>“), mit dem Code grün (`ResponsesStreamTranslatorTest` 9/9).
+
+
+### F118 – java-llama.cpp: Anthropic-Serverfehler heißen `invalid_request_error`
+- **Zeit:** 2026-10-18 11:45 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java:718` (`anthropicError`)
+- **Schwere:** niedrig
+- **Beschreibung:** `anthropicError` setzte den Fehlertyp immer auf `invalid_request_error`, auch für eine Antwort mit HTTP 500 und für das `error`-Ereignis eines gestreamten `/v1/messages`, das an einem Inferenzfehler scheiterte. Anthropics Fehlertypen trennen die Schuld: `invalid_request_error` ist der Fehler des Clients (400), `api_error` ein unerwarteter Serverfehler (500). Ein Client (Claude Code), der nach dem Typ entscheidet, ob ein Wiederholen lohnt, hielt einen Serverfehler für eine fehlerhafte eigene Anfrage – im Stream kommt der Status 200 ohnehin vor dem Fehler, dort trägt nur der Typ die Unterscheidung.
+- **Fix:** `anthropicError(message, type)`; die Ablehnungszweige (`IllegalArgumentException`/`InvalidRequestException`) behalten `invalid_request_error`, die Fehlerzweige melden `api_error`.
+- **Reproduktion:** `OpenAiCompatServerHttpTest#aFailedAnthropicMessageIsAnApiError`, `#aFailedStreamedAnthropicMessageIsAnApiError` (dazu als Schutz `#aRejectedStreamedAnthropicMessageStaysAnInvalidRequestError`)
+- **Rot-Grün-Nachweis:** ohne Fix beide rot („Expected: a string containing "\"error\":{\"type\":\"api_error\"" but: was "{"type":"error","error":{"type":"invalid_request_error","message":"decoding failed"}}"“), mit Fix grün (54/54 der Klasse, skipped 0); Gegenprobe per `git stash` rot (2), nach `pop` grün. Breite Liste wie F117 grün, `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `ae72c29`
+
+
+### F123 – java-llama.cpp: `logprobs`/`top_logprobs` einer Chat-Anfrage gehen verloren
+- **Zeit:** 2026-10-18 14:35 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java:72` (`toInferenceParameters`)
+- **Schwere:** niedrig
+- **Beschreibung:** Eine OpenAI-Chat-Anfrage mit `"logprobs":true` (und optional `"top_logprobs":N`) bekam keine Token-Wahrscheinlichkeiten: Der Mapper übernahm keins der beiden Felder, `InferenceParameters` trug also kein `n_probs`. Upstream bildet sie in `oaicompat_chat_params_parse` (`server-common.cpp:1456`, b11639) auf `n_probs = top_logprobs` ab, Standard 20, und die nativen Chat-Ergebnisse tragen die Wahrscheinlichkeiten dann von selbst (blockierend und im OAI-Chunk-Pfad). Teil des offenen F95-Verdachts („`n_probs`/`logprobs`“), der als Blocker eine API-Erweiterung nannte – `withNProbs` gab es aber schon (Erkenntnis F95: Verdacht in Teile zerlegen). Gefunden per Probe-Klasse über den Mapper.
+- **Fix:** Bei `logprobs: true` setzt der Chat-Mapper `withNProbs(top_logprobs)`, ohne `top_logprobs` 20 wie upstream. Nur im Chat-Pfad: Das `logprobs` des alten `/v1/completions` ist eine Zahl mit anderer Bedeutung. Nicht übernommen: upstreams zwei Ablehnungen (`top_logprobs` ohne `logprobs`; `logprobs` mit Werkzeugen im Stream). (**Korrigiert in F220:** die erste Ablehnung gilt jetzt mit upstreams Text, und ein ganzzahliges `logprobs` ist wie bei upstream der Alias von `n_probs` statt der Chat-Form; die zweite bleibt offen.)
+- **Reproduktion:** `OpenAiRequestMapperTest#logprobsWithTopLogprobsBecomesNProbs`, `#logprobsWithoutTopLogprobsDefaultsToTwentyLikeUpstream` (dazu als Schutz `#logprobsFalseLeavesNProbsUnset`)
+- **Rot-Grün-Nachweis:** ohne Fix beide rot („Expected: is <3> but: was <-1>“ bzw. „Expected: is <20> but: was <-1>“), mit Fix grün (31/31 der Klasse, skipped 0); Gegenprobe per `git stash` rot (2), nach `pop` grün. Breite Liste wie F117 grün, `spotless:apply` und `spotbugs:check` grün. Der erste Grün-Lauf scheiterte am Compiler (`NotJavadoc`: Konstante zwischen fremde Javadoc und Deklaration eingefügt, wie bei F108) und meldete trotzdem den alten roten Bericht – das Prüfskript löscht den Bericht jetzt auch vor dem Grün-Lauf.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `578614c`
+
+
+### F125 – java-llama.cpp: `OpenAiServerCli` nimmt einen Port außerhalb von 0..65535 an; der Fehler kommt erst nach dem Modell-Laden
+- **Zeit:** 2026-10-18 15:40 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiServerCli.java:72` (`parse`, `--port`), Aufrufer `OpenAiCompatServer.main`
+- **Schwere:** niedrig
+- **Beschreibung:** `--port`/`-p` lief nur durch `Integer.parseInt`. `-p 70000` oder `-p -1` wurde angenommen; `OpenAiCompatServer.main` lud dann zuerst das Modell (bei großen GGUFs Minuten) und scheiterte erst beim Binden in `new InetSocketAddress` mit „port out of range“ als nicht gefangener `IllegalArgumentException` samt Stacktrace. Die Javadoc von `main` verspricht für Usage-Fehler eine Meldung auf stderr und Rückkehr; ein nicht numerischer Port wird schon so behandelt, ein numerischer außerhalb des TCP-Bereichs nicht.
+- **Fix:** `portValue` prüft 0..65535 (0 bleibt erlaubt: freier Port) und wirft den üblichen Usage-Fehler.
+- **Reproduktion:** `OpenAiServerCliTest#portOutsideTheTcpRangeThrows` (`-1`, `65536`, `70000`), Grenzen in `#portAtTheEndsOfTheTcpRangeIsAccepted` (0, 1, 65535)
+- **Rot-Grün-Nachweis:** ohne Fix rot in allen drei Fällen („Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“), mit Fix grün (45/45 der Klasse, skipped 0); Gegenprobe per `git stash` rot (3), nach `pop` grün. Breite Liste: `OpenAiCompatServerHttpTest` 54/54, `ServerLauncherTest` 7/7, `LlamaArchitectureTest` 13/13; `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `72cf2ea`
+
+
+### F128 – java-llama.cpp: Gestreamte Werkzeugaufrufe in `llama-langchain4j` brechen am ersten Fragment ohne Argumente oder ohne Namen ab
+- **Zeit:** 2026-10-18 17:00 UTC
+- **Datei:** `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/StreamingChunkAssembler.java:131` (`acceptToolCallFragment`), Aufrufer `JllamaStreamingChatModel.doChat`
+- **Schwere:** mittel
+- **Beschreibung:** Jedes `tool_calls`-Fragment wurde als `PartialToolCall` an den Handler gegeben. langchain4j 1.22.0 baut ihn mit `ensureNotBlank(name)` und `ensureNotEmpty(partialArguments)` (mit `javap -c` am Jar nachgesehen). Upstreams `server_chat_msg_diff_to_json_oaicompat` lässt `arguments` weg, solange der Diff keine hat, und `common_chat_msg_diff::compute_diffs` meldet einen neuen Aufruf mit dem bis dahin geparsten Stand – das erste Fragment trägt typischerweise nur `id` und `name`, und laut Kommentar dort darf der Name anfangs leer sein. Dann warf `PartialToolCall.builder().build()` eine `IllegalArgumentException` aus dem Stream-Callback, `doChat` fing sie und rief `onError`: der ganze gestreamte Werkzeugaufruf schlug fehl. Die vorhandenen Tests schickten nur Fragmente, die schon Argumente trugen.
+- **Fix:** Ein Fragment mit leeren Argumenten oder solange der Name fehlt fließt weiter in den zusammengesetzten Aufruf, wird aber nicht als `PartialToolCall` gemeldet (wie langchain4js eigene OpenAI-Anbindung, die nur nicht leere Argumentstücke meldet).
+- **Reproduktion:** `StreamingChunkAssemblerTest#toolCallDeltaWithoutArguments_doesNotFailTheStream`, `#toolCallDeltaBeforeItsName_doesNotFailTheStream`
+- **Rot-Grün-Nachweis:** ohne Fix beide rot („Unexpected exception thrown: java.lang.IllegalArgumentException: partialArguments cannot be null or empty“ bzw. „… name cannot be null or blank“), mit Fix grün (2/2, skipped 0); Gegenprobe per `git stash` rot (2), nach `pop` grün. Ganzes Modul grün (`StreamingChunkAssemblerTest` 9/9, `LangChain4jMappingTest` 23/23, `JsonSchemaElementSerializerTest` 8/8, `TestModelPathsTest` 8/8; die Integrationstests überspringen sich ohne Modell wie zuvor). Das Modul hat weder Spotless noch SpotBugs.
+- **CI-Bedarf:** `JllamaToolCallingIntegrationTest#streamedRequiredToolCallComesBackAsToolExecutionRequest` (Job `test-java-llama-langchain4j-integration`, kein Release-Gate) im nächsten Lauf ansehen: ob er vor dem Fix rot war, hängt davon ab, ob das CI-Modell den Namen allein in einem Fragment sendet.
+- **Nachtrag (Branch-Review 2026-10-20):** Die beiden Tests oben prüften nur, dass nichts wirft, und die fertige Anfrage; welche Fragmente als `PartialToolCall` gemeldet werden und mit welcher Id, war ungeprüft (Fund J14-2). `#toolCallDeltaWithoutArguments_doesNotFailTheStream` pinnt jetzt genau ein gemeldetes Teilstück (Index 0, Id `call_1`, Name `get_weather`, Argumente `{}`), einen `CompleteToolCall` und langchain4js Vertrag über den neuen Helfer `assertPartialsMakeUpTheCompleteCall` (die Argumentstücke ergeben aneinandergehängt die Argumente des fertigen Aufrufs, jedes trägt dessen Id und Namen). `#toolCallDeltaBeforeItsName_doesNotFailTheStream` prüft nur, dass mindestens ein Teilstück gemeldet wird (`is(not(empty()))`, damit die Schleife nicht leer durchläuft; bei der Prüfung ergänzt, rot gegen eine Mutante, die nach einem zurückgehaltenen Fragment nichts mehr meldet: `Expected: is not an empty collection`) und dass jedes gemeldete Teilstück die endgültige Id und den Namen trägt: der Vertrag ist dort heute rot (`Expected: is "{\"city\":1}" but: was "ty\":1}"`, das Präfix vor dem Namen wird nie gemeldet, Fund J14-1) und kommt mit dessen Fix; das heutige einzelne Teilstück wird bewusst nicht festgeschrieben. Gegenprobe rot: die Id aus dem aktuellen Fragment statt aus dem gesammelten Aufruf (`Expected: is "call_1" but: was ""`), das Kopf-Fragment mit einem Platzhalter-Argument `" "` gemeldet (`Expected: is <1> but: was <2>`); unverändert 12/12 grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `f7f3f0f`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J14-4):** Die Klassen-Javadoc von `StreamingChunkAssembler` sagte noch, jedes `tool_calls`-Fragment gehe als `onPartialToolCall` hinaus, und dass id und Name am ersten Fragment kämen. Seit dem Fix (`f7f3f0f`) wird ein Fragment nur weitergereicht, wenn es Argumenttext trägt und sein Aufruf schon einen Namen hat; andere (ein Kopf nur mit id/Name, Argumente vor dem Namen) speisen nur den zusammengesetzten Aufruf. Javadoc und der Satz im `llama-langchain4j/README.md` sagen das jetzt, „id und Name“ kommen „usually“ am ersten Fragment. Nur Doku.
+- **Nachtrag (Branch-Review 2026-10-20/21):** Das Präfix vor dem Namen (J14-1) ist als [F265](#f265) behoben: `#toolCallDeltaBeforeItsName_doesNotFailTheStream` prüft jetzt `assertPartialsMakeUpTheCompleteCall`.
+
+
+### F133 – java-llama.cpp: `setPoolingType(UNSPECIFIED)` lässt ein vorher gesetztes `--pooling` stehen
+- **Zeit:** 2026-10-18 20:40 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/ModelParameters.java:652` (`setPoolingType`)
+- **Schwere:** niedrig
+- **Beschreibung:** Die Javadoc sagt, mit `UNSPECIFIED` werde der Parameter nicht weitergegeben und das Modell nutze seine eigene Pooling-Strategie. Der Code überspringt bei `UNSPECIFIED` nur das Setzen; ein vorher gesetzter Typ bleibt in der Map, `setPoolingType(MEAN).setPoolingType(UNSPECIFIED)` lädt das Modell also mit `--pooling mean`. `setRopeScaling(UNSPECIFIED)` entfernt die Option seit F13 – dessen Eintrag nennt `setPoolingType` als Vorbild, das es nicht war.
+- **Fix:** `UNSPECIFIED` ruft `removeOption(ModelOption.POOLING)`, wie `setRopeScaling`; Javadoc um den Satz zum Entfernen ergänzt.
+- **Reproduktion:** `ModelParametersTest#testSetPoolingTypeUnspecifiedRemovesAnEarlierPoolingType`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected: not map containing ["--pooling"->ANYTHING] but: was <{--fit=on, --pooling=mean}>“, die übrigen 92 Tests der Klasse grün), mit Fix 93/93 (skipped 0), Gegenprobe per `git stash` rot, danach wieder grün. `LlamaArchitectureTest` 13/13, `WireNameRegistryTest` 8/8, `ModelParametersExtendedTest` 148/148; `spotless:check` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `b4630d5`
+
+
+### F134 – java-llama.cpp: fünf `InferenceParameters`-Wither „`null` clears“ behalten den alten Wert
+- **Zeit:** 2026-10-18 21:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/JsonParameters.java:245` (`withOptionalJson`), Aufrufer `InferenceParameters.withPrompt`/`withInputPrefix`/`withInputSuffix`/`withGrammar`/`withToolChoice`
+- **Schwere:** mittel
+- **Beschreibung:** Die fünf Wither dokumentieren `null` als „clears“ (bei `withPrompt`: „clears any prior prompt“). Alle gehen über `withOptionalJson`, das bei `null` nur `this` zurückgibt. Ein früher gesetzter Wert bleibt also im Request: `withGrammar(g).withGrammar(null)` schickt die Grammatik weiter mit, die Generierung bleibt eingeschränkt, ebenso `tool_choice` oder ein Infill-Präfix. Der vorhandene Test `testToJsonStringNull` setzt `null` nur auf ein frisches Objekt und sieht das nicht.
+- **Fix:** `withOptionalJson(field, null)` entfernt einen vorhandenen Eintrag (neue Instanz, Original unverändert) und gibt nur dann `this` zurück, wenn der Eintrag fehlt; Javadoc angepasst.
+- **Reproduktion:** `InferenceParametersTest#test{Prompt,InputPrefix,InputSuffix,Grammar,ToolChoice}NullClearsAnEarlierValue`, `JsonParametersTest#withOptionalJson_nullRemovesAnEarlierValue`
+- **Rot-Grün-Nachweis:** ohne Fix rot – alle fünf Wither-Tests (z. B. „Expected: not map containing ["grammar"->ANYTHING] but: was <{grammar="x"}>“) und der Basistest („expected: not same but was: “); mit Fix `InferenceParametersTest` 111/111 und `JsonParametersTest` 26/26 (skipped 0), Gegenprobe per `git stash` wieder rot. PIT auf `JsonParameters`: 18/18 getötet. `LlamaArchitectureTest` 13/13, `WireNameRegistryTest` 8/8, `OpenAiRequestMapperTest` 31/31; `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `5bba245`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J8-4):** Zwei Stellen in `JsonParameters` beschrieben noch das alte Verhalten: das `@param text` von `withOptionalJson` („`null` means leave the parameter unset“) und die Javadoc von `toJsonString` („null to behave as do not set this parameter“). Beide sagen jetzt, dass `null` den früheren Wert löscht, wie die Beschreibung, das `@return` und die Aufrufer in `InferenceParameters`. Nur Doku.
+
+
+### F137 – java-llama.cpp: `Session.fork` prüft eine negative Ziel-Slot-ID erst nach Sichern und Wiederherstellen
+- **Zeit:** 2026-10-18 22:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/Session.java:250` (`fork`)
+- **Schwere:** niedrig
+- **Beschreibung:** `fork(newSlotId, filepath)` prüft vor den Modellaufrufen nur `newSlotId == slotId`. Eine negative ID erreicht `model.saveSlot(slotId, filepath)` (schreibt die Datei) und `model.restoreSlot(newSlotId, filepath)`, das im nativen Teil an der ungültigen ID scheitert – der Aufrufer bekommt eine `LlamaException` statt der `IllegalArgumentException`, die der Konstruktor für genau diesen Wert wirft, und eine geschriebene Scratch-Datei. Die Konstruktorprüfung („Validate here, not per request“) kommt erst danach zum Zug. Die Javadoc nannte als `IllegalArgumentException`-Fall nur die gleiche ID.
+- **Fix:** `fork` weist eine negative ID vor jedem Modellaufruf mit derselben Meldung wie der Konstruktor ab; Javadoc ergänzt.
+- **Reproduktion:** `SessionTest#forkToANegativeSlotIdRejectedBeforeTheModelIsTouched` (mit `null`-Modell: jeder Modellaufruf zeigt sich als `NullPointerException`)
+- **Rot-Grün-Nachweis:** ohne Fix rot („Unexpected exception type thrown, expected:  but was: “), mit Fix 3/3 (skipped 0), Gegenprobe per `git stash` rot, danach wieder grün. `SessionStateTest` 13/13, `LlamaArchitectureTest` 13/13; `spotless:apply` und `spotbugs:check` grün. Den nativen Pfad (Datei geschrieben, `LlamaException` aus `restoreSlot`) habe ich nicht laufen lassen: keine native Bibliothek und kein Modell in dieser Umgebung.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `7f79298`
+
+
+### F138 – java-llama.cpp: `OpenAiServerConfig.heartbeatMillis(0)` lässt jeden Streaming-Request als Client-Fehler scheitern
+- **Zeit:** 2026-10-18 22:55 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiServerConfig.java` (`Builder.heartbeatMillis`), Verbraucher `OpenAiCompatServer` (vier `scheduleAtFixedRate(…, getHeartbeatMillis(), getHeartbeatMillis(), …)`)
+- **Schwere:** niedrig
+- **Beschreibung:** Der Builder nimmt jeden Wert. Das Intervall ist die Periode von `scheduleAtFixedRate`, die bei ≤ 0 `IllegalArgumentException` wirft; der Streaming-Handler fängt genau diese Klasse als Fehler der Anfrage. Mit `heartbeatMillis(0)` – naheliegend als „kein Heartbeat“ – antwortet daher jeder Streaming-Request mit HTTP 200 und nur `data: {"error":{"message":"IllegalArgumentException","type":"invalid_request_error",…}}`, ohne `[DONE]` (gemessen mit dem modellfreien Testserver); nicht gestreamte Anfragen gehen. Der Fehler sieht nach einem Client-Fehler aus und nennt die Einstellung nicht.
+- **Fix:** `heartbeatMillis` weist einen nicht positiven Wert beim Konfigurieren mit `IllegalArgumentException` ab, Javadoc ergänzt.
+- **Reproduktion:** `OpenAiServerConfigTest#heartbeatIntervalOfZeroIsRejected`; das Laufzeitsymptom zeigte eine nicht übernommene Probe in `OpenAiCompatServerHttpTest` (Streaming-Chat gegen `FakeBackend` mit `heartbeatMillis(0)`).
+- **Rot-Grün-Nachweis:** ohne Fix rot („Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“), mit Fix 7/7 (skipped 0), Gegenprobe per `git stash` rot, danach wieder grün. `OpenAiCompatServerHttpTest` 54/54, `OpenAiServerCliTest` 45/45, `LlamaArchitectureTest` 13/13; `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `806571d`
+
+
+### F143 – java-llama.cpp: `RouterClient.awaitModelLoaded(id, 0)` meldet für ein geladenes Modell einen Timeout, ohne abzufragen
+- **Zeit:** 2026-10-19 01:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/RouterClient.java:210` (`awaitModelLoaded`)
+- **Schwere:** niedrig
+- **Beschreibung:** Die Schleife `while (System.currentTimeMillis() < deadline)` prüft die Frist vor der ersten Abfrage. Mit `timeoutMillis` 0 (die naheliegende Frage „ist es jetzt geladen?“) oder einer Frist, die vor der ersten Abfrage abläuft, wird `GET /models` nie gefragt: der Aufruf wirft „did not reach LOADED within 0 ms (last status: null)“ für ein geladenes Modell, und auch die dokumentierten Sofort-Fehler (Modell nicht gelistet, Worker gescheitert) treten nie ein. Dieselbe Klasse wie der schon behobene Überlauf bei `Long.MAX_VALUE` (Test `awaitModelLoaded_longMaxValueTimeoutStillPolls`: „would skip polling and report a timeout for a loaded model“).
+- **Fix:** Die Methode fragt mindestens einmal ab und prüft die Frist erst nach einer Abfrage, vor der Pause.
+- **Reproduktion:** `RouterClientTest#awaitModelLoaded_zeroTimeoutStillPollsOnce`
+- **Rot-Grün-Nachweis:** ohne Fix rot („Unexpected exception thrown: java.lang.IllegalStateException: Model 'qwen' did not reach LOADED within 0 ms (last status: null)“), mit Fix 20/20 (skipped 0, die Timeout- und Sofort-Fehler-Tests unverändert grün), Gegenprobe per `git stash` rot, danach wieder grün. `LlamaArchitectureTest` 13/13; `spotless:check` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `017ac95`
+
+
+### F158 – java-llama.cpp: `ModelParameters.setPredict(-2)` verspricht „until context filled“, der Server erzeugt damit je Anfrage nur ein Token
+- **Zeit:** 2026-10-19 00:10 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/ModelParameters.java:202` (Javadoc `setPredict`)
+- **Schwere:** niedrig bis mittel (still: keine Fehlermeldung, nur abgeschnittene Antworten)
+- **Beschreibung:** Gefunden beim Nachprüfen von F69. Upstreams `-n` (`common/arg.cpp:1661`, b11639) nennt „-2 = until context filled“ **nur** für `LLAMA_EXAMPLE_COMPLETION`, für den Server lautet die Beschreibung „-1 = infinity“; der Parser übernimmt jeden Wert. Im Server übernimmt ein Slot `params_base.n_predict`, wenn die Anfrage `-1` sendet oder das Feld weglässt (`server-context.cpp:1964`), und `has_budget()` (`:495`) zählt nur `-1` als unbegrenzt: bei `-2` ist `n_remaining()` negativ, und die Grenzprüfung (`:2040`, `n_gen > 0 && !has_budget()`) beendet die Erzeugung nach dem ersten Token mit `STOP_TYPE_LIMIT`. Aus dem Quelltext abgeleitet, **nicht gemessen**.
+- **Fix:** Dokumentation korrigiert (ohne Test, Nutzerregel): Javadoc nennt nur noch `-1 = infinity` und erklärt, dass `-2` eine Einstellung des Completion-Tools ist und hier nach dem ersten Token stoppt. **Offen, Entscheidung des Nutzers:** `-2` in `ModelParameters.setPredict` mit `IllegalArgumentException` abweisen – modellfrei testbar, ändert aber das Verhalten eines öffentlichen Setters.
+- **Messung (J1, `BUGHUNT-MEASUREMENTS.md` in `4c58e8f`):** bestätigt – `-2`: 1 Token, `stop_type="limit"`; `-1`: 2042 Tokens (bis Kontextende); `16`: 16. Dazu J2: `withNPredict(0)` erzeugt ebenfalls 1 Token, nicht 0.
+- **Nachtrag (Entscheidung des Nutzers nach der Messung):** `setPredict` lehnt Werte unter `-1` mit `IllegalArgumentException` ab; Javadoc von `setPredict` und `withNPredict` sagt „`0` verhält sich wie `1`“. Test `ModelParametersExtendedTest#testSetPredictBelowMinusOneIsRejected` (`-2`, `-3`, `Integer.MIN_VALUE`) ersetzt `testSetPredictFillContext`, dazu `testSetPredictZero`.
+- **Rot-Grün-Nachweis:** ohne die Prüfung rot (3 von 3), mit ihr grün (`ModelParametersExtendedTest`, `ModelParametersTest`, `WireNameRegistryTest`: 252 Tests).
+- **Nachtrag (Branch-Review 2026-10-20):** Die Übersicht nannte bis zum Branch-Review noch „Doku gefixt, Verhalten offen“; das Verhalten ist seit `be4d4a2` geändert (siehe oben).
+- **Status:** gefixt
+- **Commit:** cb8a3ea (Doku), be4d4a2 (Verhalten)
+
+
+### F165 – java-llama.cpp: `OpenAiCompatServer` ohne `-c` bewirbt 8192 + 2048 Tokens statt des Fensters des geladenen Modells
+- **Zeit:** 2026-10-19 14:40 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiServerCli.java` (`toServerConfig`), `OpenAiCompatServer.java` (`main`)
+- **Schwere:** mittel (ein Client, der seinen Prompt nach `/props`/`/api/show` bemisst, schickt bis zu 5× zu viel Kontext)
+- **Historie:** Die Budgets kamen nur aus `-c` (seit F70 als ganzes Slot-Fenster aufgeteilt); ohne `-c` blieben die festen Vorgaben stehen.
+- **Messung (J3, `BUGHUNT-MEASUREMENTS.md` in `4c58e8f`):** Draft-Modell ohne `-c`: `/props` `n_ctx` 10240, `/api/show` `num_ctx 10240`, `/slots` `n_ctx` 2048 (= `n_ctx_train`).
+- **Fix:** Entscheidung des Nutzers: das echte `n_ctx` bewerben. Neue Überladung `toServerConfig(supportsVision, modelFtype, slotContextSize)`; `main` übergibt `n_ctx` des ersten Slots aus `getMetricsTyped()`. Ein bekanntes Slot-Fenster geht vor `-c` (die Last kann kleiner ausfallen, etwa durch `--fit`), sonst gilt wie bisher `-c` bzw. die Vorgabe.
+- **Tests:** `OpenAiServerCliTest#loadedSlotContextIsAdvertisedWithoutContextSize`, `#loadedSlotContextWinsOverTheContextSizeArgument`, `#unknownSlotContextFallsBackToTheContextSizeArgument`.
+- **Rot-Grün-Nachweis:** mit der alten Auswahl (nur `-c`) rot (2 von 3, der Rückfalltest ist beidseitig grün), mit dem Fix grün (`OpenAiServerCliTest`, `OpenAiCompatServer*Test`: 114 Tests). Die Verdrahtung in `main` braucht ein Modell und ist hier nur übersetzt; Prüfung mit dem Befehl aus J3 (erwartet: 2048 in `/props` und `/api/show`).
+- **Status:** gefixt
+- **Commit:** 490f43b
+
+
+### F166 – java-llama.cpp: Leere Werkzeug-Argumente im Verlauf lassen die ganze Folgeanfrage mit 400 scheitern
+- **Zeit:** 2026-10-19 15:00 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/ParameterJsonSerializer.java` (`buildMessages(List)`)
+- **Schwere:** niedrig (mit dem gemessenen Modell nicht erreichbar, im Eintrittsfall aber Totalausfall der Folgeanfrage)
+- **Historie:** `buildMessages` schreibt `ToolCall.getArgumentsJson()` seit den Werkzeugaufrufen im Verlauf wörtlich.
+- **Messung (J4, `BUGHUNT-MEASUREMENTS.md` in `4c58e8f`):** Qwen2.5-1.5B liefert für ein Werkzeug ohne Parameter 20/20 `"{}"`, nie `""`; mit `""` im Verlauf antwortet der Server `InvalidRequestException: Failed to parse tool call arguments as JSON … attempting to parse an empty input`, mit `"{}"` normal.
+- **Fix:** Entscheidung des Nutzers: leere (auch nur aus Leerraum bestehende) Argumente als `"{}"` schreiben, alles andere unverändert.
+- **Tests:** `ParameterJsonSerializerTest#testBuildMessages_emptyToolCallArguments_areWrittenAsAnEmptyObject` (`""`, `" "`, `"\n"`), `#testBuildMessages_toolCallArguments_arePassedOnVerbatim`.
+- **Rot-Grün-Nachweis:** ohne den Fix rot (3 von 3), mit ihm grün (186 Tests in den Serializer-, Multimodal-, `InferenceParameters`- und `ChatRequest`-Klassen).
+- **Nicht abgedeckt (offen):** Die Übersetzungen des Servers bauen den Verlauf ohne `buildMessages`: `ResponsesApiSupport.java:218` (`asText("")`) und `AnthropicApiSupport.java:229` (`input` fehlt → `""`) können weiterhin `""` schreiben. **Nachtrag 2026-10-19:** behoben als F172.
+- **Status:** gefixt (Java-API-Pfad; Server-Übersetzungen als F172)
+- **Commit:** 7f19450
+- **Nachtrag (Branch-Review 2026-10-20, Fund J8-6):** `ParameterJsonSerializer#buildMessages(List)`, `InferenceParameters#withMessages(List)` und `ChatRequest#buildMessagesJson` sagen jetzt, dass leere oder nur aus Leerraum bestehende Tool-Call-Argumente als `{}` geschrieben werden. Genauer als der Kommentar am Fix: upstream `json::parse`t die Argumente des Verlaufs nur bei Templates, die Objekt-Argumente nehmen (`supports_object_arguments`, `common/chat.cpp:1350`, b11639); der Code-Kommentar sagt das jetzt auch. `ToolCall` sagt, dass die Argumente aus der Modellantwort unverändert bleiben und nur beim Zurückschreiben in den Verlauf aus leer `{}` wird. Nur Doku; `ParameterJsonSerializerTest` pinnt das Verhalten.
+
+
+### F167 – java-llama.cpp: `completeBatch`/`chatBatch` laufen auf dem gemeinsamen `ForkJoinPool` und nutzen auf wenigen Kernen nicht alle Slots
+- **Zeit:** 2026-10-19 15:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/LlamaModel.java` (`completeBatch`, `completeBatchWithStats`, `chatBatch`), neu `BatchDispatch.java`
+- **Schwere:** niedrig bis mittel (Durchsatz; die Javadoc versprach die `setParallel`-Slots)
+- **Historie:** Jede Anfrage lief über `CompletableFuture.supplyAsync`, also auf dem gemeinsamen Pool mit `availableProcessors() - 1` Threads, den sie zudem mit der Anwendung teilte.
+- **Messung (J5, `BUGHUNT-MEASUREMENTS.md` in `4c58e8f`):** 8 Slots, 8 Anfragen: 16 Kerne → 8 gleichzeitig (3225 ms); 4 Kerne → 3 (14067 ms); 16 Kerne mit Pool-Parallelität 3 → 3.
+- **Fix:** Entscheidung des Nutzers: `completeBatch`/`completeBatchWithStats` übergeben alle Anfragen im Aufrufer-Thread (`requestCompletion`) und empfangen dann der Reihe nach; scheitert eine Übergabe, werden die bereits übergebenen freigegeben (`releaseTask`). Für `chatBatch` gibt es keine Trennung von Übergabe und Warten (`handleChatCompletions` wartet in einem nativen Aufruf), daher läuft dort jede Anfrage auf einem eigenen Daemon-Thread eines Executors, der genau so groß ist wie der Batch und mit dem Aufruf endet. Beides in `BatchDispatch` (ohne native Bibliothek testbar, `LlamaModel` lädt sie im statischen Initialisierer). Die SpotBugs-Ausnahme für den bewussten Rethrow ist von `LlamaModel` dorthin gezogen, zwei neue mit Begründung (`HES_LOCAL_EXECUTOR_SERVICE`, `ITC_INHERITANCE_TYPE_CHECKING`).
+- **Tests:** `BatchDispatchTest` (8): alle Übergaben vor dem ersten Empfang, Übergabe im Aufrufer-Thread, Freigabe nach gescheiterter Übergabe, Empfang aller trotz Fehler, mehr gleichzeitige Anfragen als der gemeinsame Pool Threads hat (Latch über `commonPool().getParallelism() + 2`), Reihenfolge, Fehler nach den anderen, leere Eingabe.
+- **Rot-Grün-Nachweis:** mit dem alten Mechanismus (`supplyAsync` je Anfrage, keine Freigabe) rot (4 von 8; die übrigen 4 prüfen unverändertes Verhalten), mit dem Fix grün. Ein modellgestützter Lauf (J5-Probe) steht aus: erwartet `maxProcessingSlots=8` auch auf 4 Kernen für `completeBatch`.
+- **Status:** gefixt
+- **Commit:** 8130463
+- **Nachtrag (Branch-Review 2026-10-20):** `submitAllThenReceive` fing nur `RuntimeException`; ein `Error` übersprang Freigabe und restliche Empfänge (behoben als F235). Spätere Fehler eines Batches gingen ohne Spur verloren (behoben als F236, gilt auch für `runEachOnItsOwnThread`).
+
+
+### F168 – java-llama.cpp: `JllamaNative.takeStrings` gibt ein leeres Nicht-NULL-Array nicht frei
+- **Zeit:** 2026-10-19 16:30 UTC
+- **Datei:** `llama-ffm/src/main/java/net/ladenthin/llama/ffm/JllamaNative.java` (`takeStrings`)
+- **Schwere:** niedrig (8 Byte je Aufruf mit leerem Ergebnis, praktisch unerreichbar)
+- **Historie:** Die Prüfung `pointer == NULL || count == 0` vor dem `try` stammt aus der ersten Fassung der FFM-Bindungen; `jllama_rpc_server_devices` legt auch für 0 Einträge ein Array an.
+- **Messung (J8, `BUGHUNT-MEASUREMENTS.md` in `4c58e8f`):** ein absichtlich falsch ausgerichteter Nicht-NULL-Zeiger mit Anzahl 0: `jllama_strings_free` direkt bricht mit `free(): invalid pointer` ab (Exit 134), über `takeStrings` überlebt die JVM – die Freigabe wurde nie gerufen.
+- **Fix:** Entscheidung des Nutzers: bei Nicht-NULL immer freigeben. Nur noch `NULL` kehrt früh zurück; die Freigabefunktion ist über eine paketinterne Überladung `takeStrings(out, length, free)` einsetzbar.
+- **Tests:** `JllamaNativeTest#takeStringsFreesANonNullArrayEvenWhenItIsEmpty`, `#takeStringsDoesNotFreeANullArray`.
+- **Rot-Grün-Nachweis:** lokal mit JDK 25 (Toolchain) und einem selbst gebauten `libjllama.so` (b11639, statisch): mit der alten Prüfung rot (1 von 9, der neue Test), mit dem Fix grün; `mvn -f llama-ffm/pom.xml verify` grün (29 Tests, 16 modellgestützte übersprungen, kein Modell).
+- **Nachtrag (Branch-Review 2026-10-20):** Die beiden Tests oben setzen die Anzahl auf 0 und erreichen weder die Leseschleife noch die Anzahl, mit der ein gefülltes Array freigegeben wird; der einzige Test mit Einträgen (`rpcServerDevicesResolveTheCpuAndRefuseAnUnknownName`) hat genau einen, sieht also keine Reihenfolge, und eine falsche Anzahl leckt nur innerhalb von `jllama_strings_free` (Fund J13-2). Neu, als Charakterisierung des unveränderten Verhaltens: `JllamaNativeTest#takeStringsReadsEveryEntryInOrderAndFreesTheArrayWithItsCount` (zwei Arena-Strings, aufzeichnende Freigabe; `["alpha", "beta"]` und `/2`). Gegenprobe rot: Freigabe mit 0 (`Expected: is <[…/2]> but: was <[…/0]>`), rückwärts gelesen (`Expected: is <[alpha, beta]> but: was <[beta, alpha]>`); unverändert 10/10 in der Klasse grün.
+- **Status:** gefixt
+- **Commit:** 631d0ab
+
+
+### F172 – java-llama.cpp: Responses- und Anthropic-Übersetzung schreiben leere Tool-Argumente in den Verlauf
+- **Zeit:** 2026-10-19 21:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java` (`function_call`), `AnthropicApiSupport.java` (`tool_use`)
+- **Schwere:** niedrig (laut: die Folgeanfrage scheitert mit 400)
+- **Historie:** Rest von F166, dort als „Nicht abgedeckt“ vermerkt: F166 behob den Java-API-Pfad (`buildMessages`), die beiden Übersetzungen bauen den Verlauf selbst.
+- **Beschreibung:** Ein `function_call` ohne `arguments` (Responses) und ein `tool_use` ohne `input` (Anthropic, `MissingNode.toString()` ist `""`; `input: null` ergab `"null"`) landeten als `""` bzw. `"null"` in `function.arguments`. Upstream parst jedes Argument des Verlaufs mit `json::parse` und lehnt `""` mit 400 ab (J4: „attempting to parse an empty input“).
+- **Fix:** Entscheidung des Nutzers: dieselbe Regel wie F166 – leere oder nur aus Leerraum bestehende Argumente (Responses) bzw. fehlender oder `null`-`input` (Anthropic) werden `"{}"`.
+- **Tests:** `ResponsesApiSupportTest#functionCallWithoutArgumentsBecomesAnEmptyArgumentsObject` (fehlend, `""`, Leerraum), `AnthropicApiSupportTest#toolUseWithoutInputBecomesAnEmptyArgumentsObject` (fehlend, `null`).
+- **Rot-Grün-Nachweis:** ohne den Fix rot (beide, `was ""`), mit ihm grün (75 Tests der beiden Klassen).
+- **Nachtrag (Branch-Review 2026-10-20):** Auf der Responses-Route wurden auch Objekt-`arguments` zu `"{}"` (Jacksons `asText` eines Containers ist `""`); seit F225 ist ein Wert, der weder fehlt noch `null` noch ein String ist, wie upstream eine 400.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `ce6e5a7`
+
+
+### F173 – java-llama.cpp: Responses-Übersetzung verwirft `refusal`-Teile von Assistenten-Nachrichten im Verlauf
+- **Zeit:** 2026-10-19 21:45 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java` (`inputContentText`, `inputContentWithImages`)
+- **Schwere:** niedrig (still: der Verlauf verliert, dass das Modell abgelehnt hat)
+- **Historie:** Offener Verdacht vom 2026-10-18 (TODO (11)). Die Übersetzung las Textteile nur über `text`; ein `refusal`-Teil trägt seinen Text unter `refusal`.
+- **Beschreibung:** Ein Responses-Client schickt die eigene frühere Antwort mit `{"type":"refusal","refusal":"…"}` zurück. Der Teil fiel weg, eine Nachricht nur aus einer Ablehnung wurde leer. Upstream übersetzt ihn zu einem Teil vom Typ `refusal`, den der eigene Chat-Parser mit 400 ablehnt (`common/chat.cpp`, nur Text und Medien).
+- **Fix:** Entscheidung des Nutzers: als Text übernehmen. Neuer Helfer `partText` (Text aus `text`, bei Typ `refusal` aus `refusal`), genutzt von beiden Lesewegen; neben einem Bild wird er ein `text`-Teil.
+- **Tests:** `ResponsesApiSupportTest#refusalPartsOfTheHistoryAreKeptAsText` (Text + Ablehnung, nur Ablehnung), `#refusalPartsNextToAnImageAreKeptAsTextParts`.
+- **Rot-Grün-Nachweis:** ohne den Fix rot (2 von 54: `was "a"`, `was ""`), mit ihm grün (54 von 54).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `2db54c9`
+
+
+### F174 – java-llama.cpp: `/props` meldet `total_slots` fest als 1
+- **Zeit:** 2026-10-19 22:00 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiSseFormatter.java` (`propsJson`), `OpenAiServerConfig`, `OpenAiServerCli` (`toServerConfig`), `OpenAiCompatServer` (`main`, `handleProps`)
+- **Schwere:** niedrig (still falsch; ein Client, der Anfragen auf die Slots verteilt, sieht einen Slot)
+- **Historie:** Offener Verdacht vom 2026-10-18 (TODO (9)): `propsJson` setzte `total_slots` seit seiner Einführung als Konstante; upstreams `/props` meldet `params.n_parallel` (`get_res_props`).
+- **Beschreibung:** Mit `--parallel 4` – oder ohne die Option, wenn llama.cpp die Slotzahl selbst wählt – meldete `/props` einen Slot. Die Zahl kannte nur die CLI, nicht `OpenAiServerConfig`.
+- **Fix:** Entscheidung des Nutzers: `OpenAiServerConfig.totalSlots` (Builder, mindestens 1, Vorgabe 1) wird in `/props` gemeldet. Der Starter nimmt die Slotzahl des geladenen Modells (`getMetricsTyped().getSlotMetrics().size()`, wie schon das Kontextfenster je Slot, F70/J3); ohne sie entscheidet `--parallel`, ohne das 1. Neue Überladung `toServerConfig(vision, ftype, slotContext, totalSlots)`; die Metriken werden in `main` nur noch einmal abgefragt.
+- **Tests:** `OpenAiCompatServerHttpTest#propsEndpointReportsTheSlotCountOfTheConfiguration` (über HTTP), `OpenAiSseFormatterTest#propsJsonReportsContextLengthAndModalities` (ergänzt), `OpenAiServerConfigTest#totalSlotsDefaultsToOneAndKeepsTheConfiguredCount`, `#totalSlotsBelowOneIsRejected`, `OpenAiServerCliTest#slotCountOfTheLoadedModelIsAdvertised`, `#unknownSlotCountFallsBackToTheParallelArgumentAndThenToOne`.
+- **Rot-Grün-Nachweis:** mit einem Platzhalter, der das alte Verhalten behält (Getter liefert 1, `propsJson` ignoriert den Parameter), rot (5 Tests), mit dem Fix grün (129 Tests der vier Klassen); SpotBugs grün. Der Starter selbst (`main`) braucht ein Modell und ist nur übersetzt.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `cb21ecf`
+
+
+### F175 – java-llama.cpp: `RouterClient.findModel`/`awaitModelLoaded` erkennen einen Alias nicht
+- **Zeit:** 2026-10-19 22:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/RouterClient.java` (`findModel`), `value/RouterModel.java`, `json/RouterModelsResponseParser.java`
+- **Schwere:** niedrig (laut, aber falsch: Timeout bzw. „not listed“, obwohl der Router das Modell bedient)
+- **Historie:** Offener Verdacht vom 2026-10-17 (TODO (4)). Upstream löst Modellnamen über Aliase auf (`--alias` im Preset, `server_models::get_meta`) und listet sie in `GET /models` unter `aliases` (`server-models.cpp:2141`, b11639); `RouterModel` las das Feld nie.
+- **Beschreibung:** `awaitModelLoaded("")` meldete nach der vollen Wartezeit „Router does not list model“, `findModel("")` lieferte nichts – während eine Anfrage unter demselben Namen bedient wurde.
+- **Fix:** Entscheidung des Nutzers (API-Erweiterung): `RouterModel.getAliases()` und `isNamed(name)` (Id oder Alias, groß/klein genau wie upstream), ein neuer Konstruktor mit Aliasen (die bisherigen delegieren mit leerer Liste), der Parser liest `aliases`, `findModel` vergleicht über `isNamed`. README-Hinweis.
+- **Tests:** `RouterClientTest#findModel_matchesByAlias` (Stub-Router, auch `awaitModelLoaded` über den Alias), `RouterModelsResponseParserTest#parsesAliasesAndDefaultsThemToEmpty`, `RouterModelTest#aliasesDefaultToEmptyAndAreKeptUnmodifiable`, `#isNamed_matchesTheIdAndEveryAliasAndNothingElse`.
+- **Rot-Grün-Nachweis:** mit Platzhaltern (`getAliases()` leer, `isNamed` nur Id) rot (4 Tests), mit dem Fix grün (54 Tests, 2 modellgestützte übersprungen); PIT auf `RouterModel` 18/18 getötet; SpotBugs grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `0dd5e41`
+
+
+### F176 – java-llama.cpp: `rerank(String, String...)` liefert die Dokumente in Hash-Reihenfolge
+- **Zeit:** 2026-10-19 22:35 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/LlamaModel.java` (`rerank(String, String...)`), `json/RerankResponseParser.java` (`toDocumentScores`)
+- **Schwere:** niedrig (still: Reihenfolge zufällig; gleiche Dokumente teilen einen Eintrag)
+- **Historie:** Rest von F167 (TODO (15)): F167 behob den Batch-Teil, die Map-Form blieb. Sie sammelte seit jeher in einer `HashMap`, nach Dokumenttext.
+- **Beschreibung:** Die Ergebnisse kommen in Dokumentreihenfolge aus dem Server; die `HashMap` gab sie in Hash-Reihenfolge zurück (gemessen: `zeta, alpha, mid, b, a` → `mid` an zweiter Stelle). Gleiche Dokumente fallen auf einen Eintrag zusammen – ohne Datenverlust, weil gleicher Text gleich bewertet wird, aber undokumentiert.
+- **Fix:** Entscheidung des Nutzers: `LinkedHashMap` in Ergebnisreihenfolge, herausgelöst als `RerankResponseParser.toDocumentScores` (ohne native Bibliothek testbar); Javadoc von `rerank(String, String...)` nennt das Zusammenfallen und verweist für eine Liste auf `rerank(boolean, …)`. Die Signatur bleibt (`LlamaOutput` nimmt eine Map).
+- **Tests:** `RerankResponseParserTest#toDocumentScores_keepsTheOrderOfTheDocuments`, `#toDocumentScores_equalDocumentsShareOneEntryAtTheirFirstPosition`.
+- **Rot-Grün-Nachweis:** mit der `HashMap` rot (1 von 14, `item 1: was "mid"`), mit der `LinkedHashMap` grün (14 von 14); PIT auf `RerankResponseParser` 6/6 getötet; SpotBugs grün (Parameter `Collection`, vorbelegte Größe).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `ef443b0`
+
+
+### F177 – java-llama.cpp: `/v1/rerank`: `top_n: 0` liefert alle Ergebnisse, ein negatives `top_n` wird durchgereicht, `usage` fehlt
+- **Zeit:** 2026-10-19 22:55 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OaiRerankSupport.java`, `LlamaModelBackend.java` (`rerank`), `src/main/cpp/json_helpers.hpp` (`rerank_results_to_json`)
+- **Schwere:** niedrig (still abweichend von upstream)
+- **Historie:** Offener Verdacht vom 2026-10-18 (TODO (8)). `OaiRerankSupport` behandelte `top_n <= 0` seit seiner Einführung als „alle“. Upstreams `format_response_rerank` (`server-common.cpp:1535`, b11639) behält `min(top_n, n)` Einträge – bei 0 keinen, bei einem negativen Wert ein `resize` auf eine riesige Größe – und setzt immer `usage` aus der Summe von `tokens_evaluated`. Das native Array (`rerank_results_to_json`) reichte `tokens_evaluated` nicht weiter.
+- **Beschreibung:** `top_n: 0` lieferte alle Dokumente statt keines, `top_n: -1` ebenfalls alle (upstream scheitert intern), und `usage` fehlte in der OpenAI-förmigen Antwort ganz.
+- **Fix:** Entscheidung des Nutzers: `top_n: 0` behält keinen Eintrag (OpenAI- und TEI-Form); ein negatives `top_n` ist eine ungültige Anfrage (400), geprüft in `LlamaModelBackend.rerank` vor dem nativen Aufruf; `rerank_results_to_json` reicht `tokens_evaluated` weiter (0, wenn das Ergebnis es nicht trägt), und `toOaiResponse` setzt `usage.prompt_tokens`/`total_tokens` auf die Summe. Javadoc von `handleRerank` nennt das neue Feld.
+- **Tests:** C++: `RerankResultsToJson.TokensEvaluatedArePassedOnAndDefaultToZero`. Java: `OaiRerankSupportTest#negativeTopNIsAnInvalidRequest`, `#topNZeroReturnsNoResultsLikeUpstream`, `#toOaiResponseCarriesTheUsageOfTheEvaluatedTokens`, `#readTopNReturnsValueOrNegativeOne` (um 0 ergänzt).
+- **Rot-Grün-Nachweis:** C++ ohne den Fix rot (`Which is: -1`), mit ihm grün (9/9 der Suite); Java ohne den Fix rot (3 von 16), mit ihm grün (16/16, mit `OpenAiCompatServerHttpTest` 71); SpotBugs grün. Ende zu Ende mit einem Reranker (CI: `OpenAiServerRerankIntegrationTest`) nicht lokal gelaufen.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `68f81b6`
+- **Nachtrag (Branch-Review 2026-10-20):** Die Abweisung eines negativen `top_n` griff nur für die ganzzahlige Schreibweise; ein `-1.5` galt als „fehlt“. Seit F230 liest `readTopN` jede Zahl, die Abweisung deckt auch die Gleitkomma-Schreibweise ab (`OaiRerankSupportTest#negativeFloatTopNIsAnInvalidRequest`).
+- **Nachtrag (Branch-Review 2026-10-20, Fund J6-3):** Ende zu Ende nachgeholt, über die echte Route und `LlamaModelBackend.rerank`: `OpenAiServerRerankIntegrationTest#negativeTopNIsRejectedBeforeAnythingIsEvaluated` (400, `invalid_request_error`, Meldung nennt `top_n`, und `getMetricsTyped().getCumulativeProcessedPromptTokens()` bleibt gleich – der Status allein kann die Reihenfolge nicht zeigen, weil `toResponse` `top_n` nach dem nativen Aufruf noch einmal liest) und `#topNZeroAnswersNoResultsButUsageCountsTheEvaluatedTokens` (keine `results`/`data`, `usage.prompt_tokens > 0` und gleich `total_tokens` – der Beweis, dass das echte `server_task_result_rerank` `tokens_evaluated` in die Summe trägt; der C++-Test nutzt ein unechtes Ergebnis). Lokal gelaufen mit dem zufälligen 2-Layer-Llama als Reranker (`-Dnet.ladenthin.llama.rerank.model=`, Rank-Pooling über den letzten Zustand), 4/4, nichts übersprungen; CI nimmt den Jina-Reranker. Gegenprobe rot: ohne den frühen `readTopN`-Aufruf im Backend („Expected: is <419L> but: was <493L>“ beim Zähler, Status weiter 400), mit dem Stand vor F177 in Java („Expected: is <400> but: was <200>“, „Expected: is <0> but: was <2>“), ohne `usage` („Expected: a value greater than <0> but: <-1> was less than <0>“).
+- **Nachtrag (Branch-Review 2026-10-20, Fund J11-3):** Der Kommentar von `jllama_rerank` im ausgelieferten C-Header `jllama_api.h` nannte je Eintrag nur `index` und `score`; er nennt jetzt alle vier Felder, die `rerank_results_to_json` schreibt (`document`, `index` als Position in `documents`, `score`, `tokens_evaluated` mit 0, wenn upstream nichts meldet), wie die Javadoc von `LlamaModel#handleRerank`. Nur Doku.
+
+
+### F178 – java-llama.cpp: Ollama-Routen melden `done_reason` falsch und `/api/generate` keine Tokenzahlen
+- **Zeit:** 2026-10-19 23:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java`, `OpenAiCompatServer.java` (`streamOllamaChat`, `handleOllamaGenerate`)
+- **Schwere:** niedrig (still: ein Client erkennt abgeschnittene Antworten im Stream und bei `/api/generate` nie)
+- **Historie:** Offener Verdacht vom 2026-10-17 (TODO (2)); die Tokenzahlen der gestreamten `/api/chat`-Zeile kamen schon mit F98. Ollama meldet `done_reason` `"length"` für eine durch `num_predict` abgeschnittene Antwort und sonst `"stop"`, auch bei Werkzeugaufrufen.
+- **Beschreibung:** Blockierendes `/api/chat` reichte OpenAIs `finish_reason` durch, also auch `"tool_calls"`, das Ollama nicht kennt; gestreamtes `/api/chat` und beide `/api/generate`-Formen meldeten immer `"stop"`. `/api/generate` meldete außerdem weder `prompt_eval_count` noch `eval_count`, obwohl die Antwort des Backends sie trägt.
+- **Fix:** Entscheidung des Nutzers: eine Abbildung `toOllamaDoneReason` (`length` → `length`, alles andere → `stop`) für alle vier Wege. Der Stream merkt sich den `finish_reason` des Endchunks (`finishReasonOf`) für die Abschlusszeile; `/api/generate` baut aus der Antwort des Backends ein `Generated` (Text, Grund, Zählungen): aus der OpenAI-Completion `finish_reason` und `usage`, aus der nativen Infill-Antwort `stop_type` (`limit` → `length`) und `tokens_evaluated`/`tokens_predicted`. Fehlen die Zählungen, bleiben die Felder weg. `extractCompletionText`/`extractInfillContent` entfielen (nur noch von Tests benutzt).
+- **Tests:** `OpenAiCompatServerHttpTest#ollamaChatDoneReasonIsLengthWhenCutOffAndStopForToolCalls` (blockierend und gestreamt, `length`/`tool_calls`/`stop`), `#ollamaGenerateDoneReasonAndCountsFollowTheCompletion` (blockierend und gestreamt), `#ollamaGenerateWithSuffixReadsTheInfillStopType`; `OllamaApiSupportTest#generateTextExtractors` (umgestellt), `#generateWithoutCountsOmitsThemAndToolCallsAreStop`.
+- **Rot-Grün-Nachweis:** ohne den Fix rot (3 HTTP-Tests, `was "stop"`), mit ihm grün (94 Tests der beiden Klassen; Pakete `server`, `json`, `value` 417 grün); SpotBugs grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `887eb93`
+
+
+### F179 – java-llama.cpp: Ollama `/api/generate` liest weder `raw` noch `system` und vervollständigt immer roh
+- **Zeit:** 2026-10-19 23:45 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java` (`isRaw`, `toOpenAiChatRequestFromGenerate`, `Generated.fromChat`), `OpenAiCompatServer.java` (`handleOllamaGenerate`)
+- **Schwere:** niedrig (still: ein Instruct-Modell setzt den Text fort statt zu antworten, `system` und Bilder gingen verloren)
+- **Historie:** Offener Verdacht vom 2026-10-18 (TODO (16)). Die Route war seit ihrer Einführung „prompt completion / FIM“. Ollama wendet ohne `"raw": true` die Chat-Vorlage des Modells an, mit `system` als Systemnachricht; Continue (`Ollama.ts`) setzt `raw` nur, wenn der Aufrufer es verlangt.
+- **Beschreibung:** Jede Anfrage ohne `suffix` ging als roher Prompt an `/v1/completions`; `system`, `images`, `format` und `think` wurden ignoriert.
+- **Fix:** Entscheidung des Nutzers: Ollamas Semantik. Mit `suffix` wie bisher Infill, mit `raw: true` wie bisher die rohe Completion (`system` ignoriert — **korrigiert in F215:** Ollama ignoriert es nicht, sondern lehnt `raw` mit `system`, `template` oder `context` mit 400 ab; so jetzt auch hier), sonst der Chat-Weg: aus der Anfrage wird eine `/api/chat`-Anfrage (`system` + Nutzer-`prompt` mit `images`, dazu `options`, `format`, `think`) und über die vorhandene Übersetzung geschickt; die Antwort trägt `response`, `thinking` (aus `reasoning_content`), `done_reason` und die Zählungen. README und `package-info` beschreiben die drei Wege; der modellgestützte `OpenAiServerCompletionIntegrationTest` bekam eine `raw`-Variante.
+- **Tests:** `OpenAiCompatServerHttpTest#ollamaGenerateWithoutRawAppliesTheChatTemplateLikeOllama` (Aufzeichnungs-Backend: Chat-Weg, `system`, Bild, `thinking`, Zählung), `#ollamaGenerateWithRawCompletesTheBarePromptAndIgnoresSystem`; `OpenAiServerCompletionIntegrationTest#ollamaGenerateRawRoundTrip` (Modell, CI).
+- **Rot-Grün-Nachweis:** ohne den Fix rot (`was "completions"`), mit ihm grün (96 Tests); der F178-Test gab nun explizit `raw: true` an, weil er die Completion-Abbildung prüft; SpotBugs grün. Die modellgestützte Variante läuft nur in CI bzw. beim lokalen Agenten.
+- **Nachtrag (Branch-Review 2026-10-20):** Die Weitergabe von `model`, `options`, `format` und `think` in die synthetische Chat-Anfrage hatte keinen Test; jedes der vier Felder ließ sich aus `GENERATE_CHAT_FIELDS` streichen, ohne dass etwas rot wurde (Fund J3-5). `OllamaApiSupportTest#generateWithoutRawCarriesOptionsFormatAndThinkIntoTheChatRequest` pinnt `model`, `temperature` 0.2, `max_tokens` 16, `stop`, `response_format` `json_object`, `reasoning_effort` `none` und die Reihenfolge System/User; `#generateWithASchemaFormatAndNoSystemIsOneUserMessageWithAJsonSchema` den Schema-Zweig von `format` und die eine User-Nachricht ohne `system`. Gegenprobe je gestrichenem Feld rot: `model` (`Expected: is "m" but: was ""`), `options` (`Expected: is <0.2> but: was <0.0>`), `format` (`Expected: is "json_object" but: was ""`, dazu der Schema-Test), `think` (`Expected: is "none" but: was ""`); unverändert 44/44 in der Klasse grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `f12d970`
+
+
+### F180 – java-llama.cpp: OpenAI-Mapper verwerfen `logit_bias`, `n_probs` und `n` still
+- **Zeit:** 2026-10-20 00:10 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java` (`applyCommonFields`, `toCompletionParameters`), `parameters/InferenceParameters.java` (`withLogitBiasJson`)
+- **Schwere:** mittel (still: ein gesperrtes Token wurde trotzdem erzeugt, `n: 3` bekam eine Antwort, Wahrscheinlichkeiten fehlten)
+- **Historie:** Offener Verdacht vom 2026-10-18 (TODO (10)), teilweise durch F95/F123 behoben (Sampling-Felder, `logprobs`/`top_logprobs` des Chat-Wegs). Upstreams Schema liest `logit_bias` als Paar-Array oder als OpenAI-Objekt (`server-schema.cpp:382`, b11639), `n_probs` direkt und `n` als Alias von `n_cmpl` (`:62`).
+- **Beschreibung:** Chat (blockierend und gestreamt, und damit die Anthropic-, Responses- und Ollama-Übersetzungen) und das gestreamte `/v1/completions` ignorierten `logit_bias`, `n_probs` und `n`. Ein früherer Test (`unknownFieldsIgnored`) schrieb das Ignorieren von `"n":3` sogar fest. Das gestreamte `/v1/completions` baut seine Chunks aus dem erzeugten Text und kann keine Wahrscheinlichkeiten tragen.
+- **Fix:** Entscheidung des Nutzers: `logit_bias` (Objekt oder Array) wird unverändert über die neue öffentliche `InferenceParameters.withLogitBiasJson` weitergereicht (ein JSON-Wert, wie die anderen Fragment-Setter geprüft), `n_probs` über `withNProbs`; `n > 1` ist eine ungültige Anfrage (400), weil diese Wege eine Antwort liefern; auf dem gestreamten `/v1/completions` sind `logprobs`/`n_probs` eine ungültige Anfrage mit dem Hinweis auf `stream: false`, bis (14) den Stream nativ macht.
+- **Tests:** `OpenAiRequestMapperTest#logitBiasAndNProbsAreForwardedOnBothPaths`, `#moreThanOneChoiceIsRefusedBecauseOneAnswerComesBack`, `#probabilitiesOnTheStreamedCompletionAreRefused`, `#unknownFieldsIgnored` (ohne `n`); `InferenceParametersTest#withLogitBiasJson_storesTheValueAndRefusesASecondOne`.
+- **Rot-Grün-Nachweis:** mit Platzhalter-Setter und unverändertem Mapper rot (4 Tests), mit dem Fix grün (327 Tests der Mapper-, Parameter-, Registry- und Server-Klassen); SpotBugs grün.
+- **Nachtrag (Branch-Review 2026-10-20):** Die Beschreibung stimmte für die Responses-Übersetzung nicht: deren Feldliste `CHAT_PASSTHROUGH_FIELDS` filtert, was den Mapper erreicht, und enthielt `logit_bias`/`n_probs` nicht; nachgezogen mit F226.
+- **Nachtrag (Branch-Review 2026-10-20):** Zwei der Tests oben gibt es so nicht mehr, beide durch F186 (`4fb82c1`): `#logitBiasAndNProbsAreForwardedOnBothPaths` heißt `#logitBiasAndNProbsAreForwarded` (nur noch der Chat-Weg baut Parameter im Mapper), und `#probabilitiesOnTheStreamedCompletionAreRefused` ist absichtlich entfernt, weil das gestreamte `/v1/completions` seitdem Wahrscheinlichkeiten liefert (`CApi.OaiCompletionsStreamDeliversTextCompletionChunksEndingWithUsage` in `test_c_api.cpp` fordert `logprobs` an und prüft sie in den Chunks).
+- **Status:** gefixt (der Rest unter (14), Wahrscheinlichkeiten im gestreamten `/v1/completions`, mit F186)
+- **Commit:** java-llama.cpp `cceaaa0`
+
+
+### F181 – java-llama.cpp: JNI-Schalen geben Fehlermeldungen als Modified UTF-8 zurück
+- **Zeit:** 2026-10-20 01:10 UTC
+- **Datei:** `llama/src/main/cpp/jni_utf8.hpp` (neu), `jllama.cpp` (`throw_api_status`, Modell-Laden), `jni_helpers.hpp` (`report_cxx_exception_impl`), `native_server.cpp`, `rpc_bridge.cpp`, `train_engine.cpp`
+- **Schwere:** niedrig bis mittel (still verfälschte Meldung auf HotSpot, Prozessabbruch unter Android-CheckJNI)
+- **Historie:** Offener Verdacht vom 2026-10-18 (Musterdurchsicht nach F71, TODO (6)). Gemessen auf HotSpot 21 mit einer eigenen JNI-Sonde: `GetStringUTFChars` liefert für U+1F600 `ED A0 BD ED B8 80` (CESU-8), `NewStringUTF` macht aus `F0 9F 98 80` ein falsches Zeichen und verschluckt Folgebytes.
+- **Beschreibung:** `ThrowNew` und `NewStringUTF` lesen Modified UTF-8. Die Meldungen aus llama.cpp zitieren Pfade und Anfragetext in Standard-UTF-8. Die Trainer-Meldung für `missing-😀.gguf` kam als `missing-`, U+00F0 U+009F U+0098 U+0080 und `.g` an (der Rest des Pfads fehlte). Die Eingabeseite des Trainers (`GetStringUTFChars`) ist dagegen nicht erreichbar: `TrainingParameters.escapeNonAscii` (`4867189`) schreibt jedes Nicht-ASCII-Zeichen als `\u`-Escape (Jacksons `ESCAPE_NON_ASCII` ist standardmäßig aus), die Konfiguration ist also dort ASCII, wo sich die Kodierungen unterscheiden (gemessen: `"Hallo \\ud83d\\ude00 Welt"`). Die Fehlermeldungen von Anfragen an das Modell zitieren in den gemessenen Fällen keinen Anfragetext (Probe mit einem Zufallsmodell).
+- **Fix:** Neuer Header `jni_utf8.hpp` (nur `jni.h`, keine gecachten IDs, deshalb auch in `train_engine.cpp` nutzbar): `throw_utf8_impl` und `new_string_utf8_impl`. ASCII bleibt bei `ThrowNew`/`NewStringUTF`, alles andere geht als Bytes durch `String(byte[], "UTF-8")`, und Javas Dekoder ersetzt ungültige Folgen durch U+FFFD. Umgestellt sind alle Stellen mit fremdem Text: `throw_api_status`, das Modell-Laden, `report_cxx_exception_impl` (die JNI-Ausnahmegrenze), `NativeServer`, `RpcServer` und der Rückgabetext von `LlamaTrainer`. Stellen mit literalem ASCII-Text bleiben, wie sie sind. Die Eingabeseite des Trainers bekommt einen Kommentar, warum sie sicher ist.
+- **Tests:** C++ `JniUtf8.*` (6 in `test_jni_helpers.cpp`: ASCII bleibt bei `ThrowNew`, 4-Byte-Zeichen und ungültiges Byte erreichen den Konstruktor als Originalbytes, dasselbe für `new_string_utf8_impl` und für `jni_guard_impl`); Java `LlamaTrainerTest#aFourByteCharacterInAPathComesBackIntactInTheErrorMessage` (modellfrei, braucht nur `libjllama`; überspringt, wo die Dateinamen-Kodierung den Pfad nicht darstellen kann) und `#aFourByteCharacterInTheTrainingTextReachesTheTrainer` (hält die Maskierung durch `escapeNonAscii` fest, von der die Eingabeseite abhängt).
+- **Rot-Grün-Nachweis:** C++: mit durchreichenden Platzhaltern 4 von 6 rot, mit dem Fix 6 von 6 grün, die ganze Suite 640/640. Java gegen die alte Bibliothek rot (`but: was "…/missing-ð.g"`), gegen die neu gebaute grün; `LlamaLoggerTest`, `NativeLibraryLoadSmokeTest`, `RpcServerTest` grün.
+- **Nachtrag (Branch-Review 2026-10-20):** Der Satz „Die Fehlermeldungen von Anfragen an das Modell zitieren in den gemessenen Fällen keinen Anfragetext“ gilt für b11639 nicht: upstream zitiert ihn z. B. in `server-common.cpp` (`response_format type must be one of … but got: `), `common/chat.cpp` (`Missing 'role' in message: `) und im Schema-Konverter (`cannot resolve $ref `), alle drei über `throw_api_status`. Diesen Weg hatte kein Java-Test; ein Rückbau auf `ThrowNew` blieb grün (Fund J11-1). Neu, modellfrei: `NativeLibraryLoadSmokeTest#anErrorMessageKeepsAFourByteCharacterOfTheRequest` (`LlamaModel.jsonSchemaToGrammar("{\"$ref\":\"#/$defs/😀\"}")`, die `LlamaException` muss `#/$defs/😀` enthalten). Gegenprobe mit `env->ThrowNew` in `throw_api_status`: rot (`got: JSON schema conversion failed: JSON schema error at #: cannot resolve $ref #/$defs/ð\x9f\x98\x80, $defs not fo`, der Rest der Meldung fehlt); mit `throw_utf8_impl` 7/7 in der Klasse grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `50b45f8`
+- **Nachtrag (Branch-Review 2026-10-20, Funde J9-5/J11-2):** Beschreibung und Tests schrieben die Maskierung der Eingabeseite Jackson zu; sie kommt von `TrainingParameters.escapeNonAscii` (`4867189`), die gemessene Ausgabe mit kleingeschriebenem Hex (`\ud83d`) ist deren `%04x`. Jackson maskiert nur U+0000 (als Steuerzeichen) selbst. Beide Zeilen oben sind korrigiert, ebenso der Kommentar in `train_engine.cpp` und die Javadoc von `LlamaTrainerTest#aFourByteCharacterInTheTrainingTextReachesTheTrainer`, die jetzt `TrainingParametersTest#nonAsciiTextIsEscapedSoTheJsonCrossesJniIntact` als Unit-Wächter nennt. Nur Doku/Kommentar.
+- **Nachtrag (Branch-Review 2026-10-20, Fund JF-1):** Der Zweig `INVALID_REQUEST` von `throw_api_status` hatte nur den modellfreien Wächter über `jsonSchemaToGrammar` (dort `LlamaException`). Neu, modellgestützt: `ErrorHandlingTest#anInvalidRequestKeepsAFourByteCharacterOfTheRequest` – `handleChatCompletions` mit `response_format.type` U+1F600 muss `InvalidRequestException` werfen, deren Meldung upstreams `but got: 😀` (`server-common.cpp:1318`, `oaicompat_chat_params_parse`) unversehrt enthält. Gegenprobe mit `env->ThrowNew` in `throw_api_status` (nur gebaut, nicht committet): rot (`Error should quote the type intact but was: response_format type must be one of "text" or "json_object", but got: ? ==> expected:  but was: `); mit `throw_utf8_impl` am HEAD grün (Winzmodell, ngl 0). In der Klasse bleibt mit dem Winzmodell `#testHandleInfillMissingInputSuffix` rot, weil das Zufallsmodell keine FIM-Token hat (`Model does not support fill-in-the-middle infill`), unabhängig von diesem Test.
+
+
+### F182 – java-llama.cpp: Gestreamte Anfragen antworten mit 200, bevor das Backend läuft
+- **Zeit:** 2026-10-20 01:50 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java` (`ResponseStream`, die fünf Streaming-Methoden); `llama-atmosphere-agent` (Tests, README)
+- **Schwere:** mittel (eine abgelehnte Anfrage kam als `200` an; ein Client, der nur den Status prüft, sah eine leere Antwort, der Atmosphere-Agent beendete den Zug still)
+- **Historie:** Offener Verdacht vom 2026-10-17 (TODO (3)), damals als Designabweichung zur Entscheidung gestellt und durch `nativeRejectionOfAStreamedChatCompletionIsAnInvalidRequestEvent` festgeschrieben. Upstream (`server-context.cpp:4906`, b11639) holt vor dem Streamen das erste Ergebnis und antwortet bei einem Fehler mit dessen Status.
+- **Beschreibung:** Jede der fünf Streaming-Routen (`/v1/chat/completions`, `/v1/completions`, `/api/chat`, `/v1/messages`, `/v1/responses`) schickte `200` und die Stream-Header vor dem Aufruf des Backends; Anthropic und Responses schrieben zusätzlich sofort ihre Eröffnungsereignisse. Eine sofort abgelehnte Anfrage (`InvalidRequestException`, `IllegalArgumentException`) oder ein sofortiger Fehler wurde dadurch zum Fehlerereignis unter `200`.
+- **Fix:** `ResponseStream` beginnt die Antwort erst mit dem ersten Schreiben: Status `200`, Header, dann der mit `startWith` gesetzte Vorspann (die Eröffnungsereignisse von Anthropic und Responses), dann der Text. Ein Heartbeat ist ebenfalls ein Schreiben, denn er hält einen Client über einen langen Prompt hinweg; danach bleibt ein Fehler ein Ereignis. Vorher beantwortet `answerIfNotStarted` die Anfrage mit `400`/`500` und dem Fehlerformat der Route (OpenAI, Ollama `{"error"}`, Anthropic `invalid_request_error`/`api_error`), wie ohne `stream`. Nach einer solchen Antwort sind weitere Schreibversuche wirkungslos (ein Heartbeat zwischen Antwort und Abbruch schreibt nichts).
+- **Tests:** `OpenAiCompatServerHttpTest#aRejectionBeforeTheFirstChunkIsA400OnEveryStreamingRoute`, `#aFailureBeforeTheFirstChunkIsA500OnEveryStreamingRoute`, `#aRejectionAfterTheFirstChunkIsAnInvalidRequestEvent`, `#aRejectionAfterAHeartbeatIsAnEvent`; die vier bestehenden Ereignis-Tests (Responses `response.failed`, Anthropic-Fehlertypen) laufen jetzt mit einem Backend, das nach einem Chunk scheitert (`ScriptedStreamBackend`). Im Agenten `AtmosphereWireContractTest#engineFailureBeforeTheFirstChunkFailsTheTurn` (neu) und `#midStreamEngineFailureCompletesSilentlyRatherThanErroring` (jetzt wirklich nach einem Chunk).
+- **Rot-Grün-Nachweis:** vor dem Umbau rot (die beiden Vor-dem-ersten-Chunk-Tests), danach 63/63 in `OpenAiCompatServerHttpTest` und alle Server-Testklassen grün; der Agent gegen den installierten neuen Kern: vorher rot (`API returned 500: model exploded` statt `null`), nach der Aufteilung `verify` grün (278 Tests, 63 übersprungen, modell- oder bildschirmgebunden); SpotBugs grün.
+- **Nachtrag (Branch-Review 2026-10-20):** `#aRejectionAfterAHeartbeatIsAnEvent` hing an der Uhr: das Backend schlief 300 ms und warf, das 200 kam nur, wenn der 50-ms-Heartbeat in dieser Zeit lief (Fund J1-5). Das Backend wartet jetzt auf einen Latch, den der Test erst zählt, wenn er `: ping` aus dem zeilenweise gelesenen Stream gelesen hat; danach `startsWith(": ping")`, `invalid_request_error` und der Ablehnungsgrund im Text. Dasselbe für das ältere `#streamingEmitsHeartbeatsDuringAGap` (`SlowFakeBackend`, gleiche 50-gegen-300-ms-Form, vorher nur `containsString(":")`). Gegenprobe: mit einem Heartbeat, der nicht schreibt, sind beide deterministisch rot (`Expected: is <200> but: was <500>`, das Backend gibt nach 30 s auf); unverändert 69/69 grün.
+- **Nachtrag (Branch-Review 2026-10-20):** Die beim Fix umgeschriebene Fehlerzeile der Agent-README (`llama-atmosphere-agent/README.md`, „error before the stream starts (401, 413, 500, …)“ | *wire*) nannte für 413 eine Abdeckung, die kein Agent-Test hatte; die 413 prüfte nur `OpenAiCompatServerHttpTest#oversizedRequestBodyRejectedWith413` im Kern (Fund J15-1). Statt die Zeile zu kürzen, macht `AtmosphereWireContractTest#oversizedRequestIsRejectedBeforeReachingTheBackend` sie wahr: ein Server mit `maxRequestBodyBytes(64)` (der Systemprompt allein ist länger), der Zug endet mit einem Fehler, der „Request body too large“ enthält, ohne Text, und das Backend sieht keine Anfrage. Gegenprobe gegen einen eigens installierten Kern: beide Grenzen aus (`getMaxRequestBodyBytes()` liefert `Integer.MAX_VALUE`) rot (`Expected: is "" but: was "never"`); nur die `Content-Length`-Vorprüfung aus bleibt grün, weil das begrenzte Lesen ebenfalls 413 antwortet – der Test pinnt den Vertrag, nicht eine der beiden Grenzen. Ob Atmosphere eine 4xx nicht wiederholt, sieht der Test nicht (`RetryPolicy.NONE`). Unverändert 8/8 in der Klasse grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `9587087`
+
+
+### F183 – java-llama.cpp: Anthropic `stop_sequence` ist immer `null`
+- **Zeit:** 2026-10-20 02:25 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java` (`toOpenAiChatRequest`, `toAnthropicResponse`, `stopSequenceOf`, `messageDeltaEvent`), `AnthropicStreamTranslator.java`, `OpenAiRequestMapper.java`, `parameters/InferenceParameters.java` (`withVerbose`), `parameters/RequestField.java` (`VERBOSE`)
+- **Schwere:** niedrig (ein Client mit mehreren `stop_sequences` erfuhr nicht, welche gegriffen hat, und konnte einen Stopp durch das Wort nicht von einem Ende des Modells unterscheiden)
+- **Historie:** Offener Verdacht vom 2026-10-18 (TODO (12)). Upstream setzt `stop_sequence` in `to_json_anthropic` und `to_json_anthropic_stream` aus `stopping_word` (`server-task.cpp:778`/`:959`, b11639); `stop_reason` bleibt `end_turn`. Das Feld `verbose` liest upstreams Schema (`server-schema.cpp:17`), `__verbose` enthält dann `to_json_non_oaicompat()` mit `stopping_word` (blockierend am Objekt, gestreamt am ersten Chunk des Endergebnisses).
+- **Beschreibung:** `/v1/messages` übersetzt aus der OpenAI-Antwort, und die trägt das Stoppwort nicht; `stop_sequence` war deshalb fest `null`, blockierend wie gestreamt.
+- **Fix:** Die übersetzte Anfrage verlangt `verbose: true`; `stopSequenceOf` liest `__verbose.stopping_word` (leer = `null`), `toAnthropicResponse` und der Stream-Übersetzer setzen es als `stop_sequence`, der `message_delta` trägt es. `__verbose` erreicht den Client nicht, weil beide Antworten neu aufgebaut werden. Dafür gibt es `RequestField.VERBOSE` (Vertrag `SCHEMA`, vom C++-Vertragstest gegen upstreams Schema geprüft) und `InferenceParameters.withVerbose`; der OpenAI-Mapper reicht ein `verbose` des Clients weiter.
+- **Tests:** `AnthropicApiSupportTest#theTranslatedRequestAsksForTheStopWord`, `#theMatchedStopWordIsTheStopSequence`, `#noMatchedStopWordIsANullStopSequence`; `AnthropicStreamTranslatorTest#theMatchedStopWordIsTheStopSequenceOfTheMessageDelta`, `#withoutAStopWordTheStopSequenceIsNull`; `OpenAiRequestMapperTest#verboseIsForwarded`; `InferenceParametersTest` (`withVerbose`); C++ `test_wire_contracts.cpp` (`verbose` im Schema, 5/5).
+- **Rot-Grün-Nachweis:** mit wirkungslosem Platzhalter-Setter rot (5 Tests, die beiden Null-Fälle waren schon grün), mit dem Fix grün (251 Tests der berührten Klassen, `OpenAiCompatServerHttpTest` und `WireNameRegistryTest` eingeschlossen); SpotBugs grün. Zusätzlich Ende-zu-Ende mit einem Zufallsmodell über den echten Server: ohne Stoppwort `max_tokens`/`null`, mit einem Stück der eigenen Greedy-Ausgabe als Stoppwort blockierend und gestreamt `end_turn` mit `stop_sequence: "j%j"`, kein `__verbose` in der Antwort.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `2b62481`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J8-3):** Die Javadoc von `InferenceParameters#withVerbose` sagte, `__verbose` stehe am letzten Chunk eines Streams. Upstream (`server-task.cpp`, b11639) setzt es im Chat-Stream an den ersten Chunk des Endergebnisses (`deltas.front()`: ein verbliebenes Tool-Call-Delta, sonst der Chunk mit `finish_reason`), nie an den Usage-Chunk, den `stream_options.include_usage` anhängt und den `OpenAiCompatServer` auf den Übersetzer-Routen immer verlangt; im Text-Completion-Stream an jeden Chunk; die native Antwort trägt es nicht, sie ist das Objekt selbst. Die Javadoc sagt das jetzt, die Klassen-Javadoc von `ChatStreamChunkParser` nennt die Reihenfolge des Endergebnisses (Tool-Call-Deltas, `finish_reason`, Usage). Nur Doku; `AnthropicStreamTranslatorTest#theMatchedStopWordIsTheStopSequenceOfTheMessageDelta` pinnt das Lesen an einem nicht letzten Chunk.
+
+
+### F184 – java-llama.cpp: Mehrere Prompts in einer Anfrage: alle außer dem ersten still verworfen
+- **Zeit:** 2026-10-20 02:50 UTC
+- **Datei:** `llama/src/main/cpp/jllama_engine.cpp` (`populate_completion_task`)
+- **Schwere:** mittel (still: zwei Prompts, eine Antwort; ein Client, der nach `index` zuordnet, wartet auf eine Antwort, die nie kommt)
+- **Historie:** Gefunden bei der Arbeit an TODO (14): `populate_completion_task` übernimmt nur `tokenized_prompts[0]`, und alle drei Wege, die es nutzen (blockierendes `/v1/completions` über `completions_oai`, `handleCompletions`, der Roh-Stream), posten genau eine Aufgabe. Upstream (`handle_completions_impl`, b11639) legt pro Prompt eine Aufgabe an und antwortet mit einer Wahl je Prompt.
+- **Beschreibung:** `{"prompt": ["Hi", "Ho"]}` lieferte `200` mit einer einzigen Wahl für `Hi`; `Ho` verschwand ohne Hinweis. Eine Token-Liste (`[72, 105]`) ist dagegen ein einziger Prompt.
+- **Fix:** Mehr als ein Prompt ist eine ungültige Anfrage (`400`, `a request takes one prompt, this one has N prompts; send them as separate requests`). Mehrere Prompts wirklich zu bedienen hieße mehrere Aufgaben je Anfrage und, gestreamt, eine Zusammenführung nach `index` – für keinen Weg hier vorhanden, und keiner der Clients, für die der Server gebaut ist, schickt das.
+- **Tests:** C++ `CApi.SeveralPromptsInOneRequestAreRefusedNotDropped` (`test_c_api.cpp`, Zufallsmodell): zwei Prompts auf allen drei Wegen abgelehnt, mit der Anzahl in der Meldung; eine Token-Liste und ein Array mit einem Prompt angenommen.
+- **Rot-Grün-Nachweis:** vor dem Fix rot (alle drei Wege `JLLAMA_STATUS_OK`), danach grün; ganze C++-Suite 641/641.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `ffb3a8c`
+
+
+### F185 – java-llama.cpp: OpenAI-Antworten ohne `id` und ohne `model`
+- **Zeit:** 2026-10-20 03:20 UTC
+- **Datei:** `llama/src/main/cpp/jllama_engine.cpp` (`name_completion_task`, aufgerufen von `blocking_completion` und `streaming_request`); `args/ModelOption.java` (`ALIAS`), `parameters/ModelParameters.java` (`setAlias`), `server/OpenAiServerCli.java` (`toModelParameters`)
+- **Schwere:** niedrig bis mittel (still: ein Client, der Antworten oder Stream-Chunks nach `id` zuordnet oder das Modell aus `model` liest, bekam leere Zeichenketten)
+- **Historie:** Gefunden bei der Arbeit an TODO (14), als die OAI-Antworten mit einem Zufallsmodell angesehen wurden. Upstream setzt in `handle_completions_impl` (`server-context.cpp:4884`, b11639) `oaicompat_cmpl_id = gen_chatcmplid()` und `oaicompat_model = meta->model_name` für jede Aufgabe; die Engine setzte keins von beiden, seit sie die Aufgaben selbst baut.
+- **Beschreibung:** `/v1/chat/completions`, `/v1/completions` und jeder Stream-Chunk (also auch die Übersetzungen) trugen `"id":""` und `"model":""`. Nach dem Engine-Fix hätte `OpenAiCompatServer --model-id x` außerdem in `/v1/models` `x` gemeldet, in den Antworten aber den Dateinamen, weil die CLI die Id nicht an llama.cpp weitergab.
+- **Fix:** `name_completion_task` setzt beide Felder wie upstream (eine `chatcmpl-`-Id je Aufgabe, also je Stream eine). Neu `ModelOption.ALIAS` (`--alias`, Vertrag `SERVER_PARSER`, vom C++-Vertragstest gegen upstreams Parser geprüft) und `ModelParameters.setAlias`; die CLI reicht `--model-id` als Alias weiter.
+- **Tests:** C++ `CApi.OpenAiAnswersCarryAnIdAndTheModelName` (`test_c_api.cpp`, Zufallsmodell: blockierende Completion und Chat mit `chatcmpl-`-Id und dem Modellnamen aus den Meta-Daten, verschiedene Ids, ein Stream mit genau einer Id über alle Chunks); Java `ModelParametersTest#testSetAlias`, `OpenAiServerCliTest#anExplicitModelIdBecomesTheModelsAlias`; `test_model_flags.cpp` (`--alias` im Server-Parser).
+- **Rot-Grün-Nachweis:** C++ vor dem Fix rot (`id` und `model` leer), danach grün, Suite 642/642; Java mit wirkungslosem Platzhalter rot (2), danach grün (304 Tests der Parameter-, CLI- und Registry-Klassen). Ende-zu-Ende über den echten Server mit `--model-id my-model`: `/v1/models` und die Chat-Antwort nennen beide `my-model`.
+- **Nachtrag (Branch-Review 2026-10-20):** `OpenAiServerCliTest#anExplicitModelIdBecomesTheModelsAlias` heißt seit F189 (`5fff6b8`) `#theAdvertisedModelIdBecomesTheModelsAliasWithAndWithoutTheFlag` und prüft den Fall ohne `--model-id` mit (siehe F189).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `a8faaef`
+
+
+### F186 – java-llama.cpp: Das gestreamte `/v1/completions` ist ein selbstgebauter Stream ohne `usage`, Wahrscheinlichkeiten und Token-Prompt
+- **Zeit:** 2026-10-20 04:00 UTC
+- **Datei:** `llama/src/main/cpp/jllama_engine.{hpp,cpp}` (`completions_oai_stream_request`), `jllama_api.{h,cpp}` (`jllama_completions_oai_stream_request`), `jllama.cpp` + `jllama.h` (`requestCompletionOaiStream`), `LlamaModel.java` (`streamCompletionsOai`), `server/LlamaModelBackend.java` (`streamCompletions`), `server/OpenAiRequestMapper.java` und `OpenAiSseFormatter.java` (toter Completion-Weg entfernt); `llama-ffm` (`JllamaNative`, `FfmLlamaModel.completionsOaiStream`); `docs/c-api.md`, README
+- **Schwere:** mittel (dieselbe Anfrage bedeutete gestreamt etwas anderes als blockierend: ohne `usage`, ohne `logprobs`, Token-Liste abgelehnt)
+- **Historie:** Offener Verdacht vom 2026-10-18 (TODO (14)); F180 hatte `logprobs`/`n_probs` auf diesem Weg ausdrücklich abgelehnt, bis der Stream nativ ist. Upstream legt `usage` auf den letzten Chunk (`to_json_oaicompat`, `server-task.cpp:391`, b11639) und streamt `logprobs` je Chunk (`:1068`).
+- **Beschreibung:** `LlamaModelBackend.streamCompletions` lief über `LlamaModel.generate` und baute `text_completion`-Chunks aus dem erzeugten Text: der letzte trug kein `usage`, Wahrscheinlichkeiten fehlten, und `OpenAiRequestMapper.toCompletionParameters` lehnte einen `prompt` als Array ab, den das blockierende `/v1/completions` (wörtlich an upstream) annimmt.
+- **Fix:** Neuer nativer Einstieg, der den Körper wie das blockierende `completions_oai` mit `oaicompat_completion_params_parse` liest und als `TASK_RESPONSE_TYPE_OAI_CMPL` streamt; der Empfang ist der vorhandene Hüllen-Empfang des Chat-Streams. `LlamaModelBackend.streamCompletions` reicht den Körper (mit `stream: true`) wörtlich weiter. `toCompletionParameters` samt seiner Ablehnungslisten und `OpenAiSseFormatter.completionChunk` hatten danach keinen Aufrufer mehr und sind entfernt; die Mapper-Tests, die gemeinsame Felder prüften, laufen jetzt über den Chat-Weg. Mehrere Prompts lehnt der native Weg seit F184 ab, gestreamt wie blockierend.
+- **Tests:** Java `OpenAiServerCompletionIntegrationTest#aStreamedCompletionIsUpstreamsStream` (modellgestützt: Token-Prompt, `logprobs`, eine Id, `usage` mit `prompt_tokens` 2, `finish_reason`, `[DONE]`); C++ `CApi.OaiCompletionsStreamDeliversTextCompletionChunksEndingWithUsage` (Zufallsmodell, dazu die Ablehnungen ohne Prompt, mit zwei Prompts und mit Null-Argument); FFM `FfmLlamaModelIntegrationTest#oaiCompletionStreamEndsWithUsage`; `OpenAiRequestMapperTest` umgestellt (33).
+- **Rot-Grün-Nachweis:** Der Java-Integrationstest mit einem Zufallsmodell vor dem Umbau rot (`400` für den Token-Prompt), danach grün; C++ 643/643; `llama-ffm verify` grün (17 + 4 + 9) gegen die neu gebaute Bibliothek; `OpenAiCompatServerHttpTest` 63, `NativeLibraryLoadSmokeTest` 6 grün.
+- **Nachtrag (Branch-Review 2026-10-20):** Der Abbruchweg hatte keinen Test: die IOException der Sink kreuzt `streamCompletionsOai` als `UncheckedIOException`, dessen `finally` den nativen Task abbricht, und `LlamaModelBackend` packt sie wieder aus, damit der Server den Disconnect-Zweig (DEBUG) nimmt und nicht den Fehlerzweig (WARN plus Fehler-Event an den gegangenen Client) (Fund J2-7). Das doppelte Weiterreichen aus `stream` und `streamCompletions` ist jetzt der paketprivate Helfer `LlamaModelBackend.relay`, modellfrei getestet in `LlamaModelBackendTest` (die IOException der Sink kommt als dieselbe Instanz zurück und beendet den Modellaufruf, eine fremde `UncheckedIOException` und jede andere RuntimeException laufen unverändert durch). Modellgestützt in `OpenAiServerCompletionIntegrationTest`: `#aSinkThatFailsMidStreamCancelsTheNativeTask` (eine Sink, die beim ersten Chunk wirft; dieselbe Exception-Instanz, dann Warten bis kein Slot mehr arbeitet, 30 s) und `#aClientThatDisconnectsMidStreamIsADisconnectAndFreesTheSlot` (roher Socket, nach der ersten `data:`-Zeile geschlossen; LogCaptor: genau ein DEBUG „client disconnected during stream“, kein WARN „streaming completion failed“). Gezählt wird über `getDecodeTotal()` bei festem Budget (100 Token, `ignore_eos`): das Warten auf einen freien Slot allein ist auch ohne den Abbruch grün, der Slot erzeugt dann nur erst das ganze Budget. Das Budget muss in den Kontext eines Slots passen; mit 200 Token hielt der Slot des Winzmodells (128) von selbst an, und ein fehlender Abbruch war nicht zu unterscheiden. Gegenprobe mit dem Winzmodell: ohne den `finally`-Abbruch in `streamCompletionsOai` sind beide rot (`decodes of the cancelled stream: 100` / `decodes of the disconnected stream: 100`, `Expected: is  but: was `); ohne das Auspacken in `relay` sind `LlamaModelBackendTest#anIOExceptionOfTheSinkComesBackAsTheSameInstance` (`expected:  but was: `) und der Socket-Test rot (`[] Expected: is <1> but: was <0>`); unverändert grün in vier Wiederholungen. Die LEST-Unterdrückung in `spotbugs-exclude.xml` nennt jetzt `relay`.
+- **Nachtrag (Branch-Review 2026-10-20):** Das Zählkriterium des vorigen Nachtrags war ein Wettlauf (Nachprüfung zu Fund J2-7, 2026-10-21): der native Worker dekodiert ohne Gegendruck weiter, während die Java-Seite den Abbruch erst nach dem ersten Chunk absetzt; mit intaktem Abbruch maß die Prüfung 2 bis 68 bzw. 4 bis 85 Dekodierschritte von 100, und einer von fünf Läufen war rot (`decodes of the disconnected stream: 85`). Jetzt endet der Stream nie von selbst: `ModelFlag.CONTEXT_SHIFT` (`--context-shift`, vom Server-Parser angenommen, `test_model_flags.cpp` grün) und `ModelParameters.enableContextShift()` sind neu (upstream schaltet die Kontextverschiebung bei b11639 standardmäßig ab, `common.h`: `ctx_shift = false`; die Javadoc von `disableContextShift()` behauptete das Gegenteil und ist berichtigt; jede der beiden Methoden entfernt jetzt die Gegenflagge, `ModelParametersExtendedTest#testTheLaterContextShiftCallReplacesTheEarlierOne`, rot ohne das Entfernen: `Expected: not map containing ["--context-shift"->ANYTHING]`). Das Testmodell der Klasse verschiebt seinen Kontext, beide Streams haben kein Token-Limit und `ignore_eos`, und das Kriterium ist allein das Warten auf einen freien Slot (30 s); `getDecodeTotal()` und das Budget sind entfallen. Gegenprobe mit dem Winzmodell: ohne den `finally`-Abbruch in `streamCompletionsOai` sind beide rot (`a slot is still processing after 30000 ms`, der Socket-Test auch allein, im abschließenden Warten), ohne das Auspacken in `relay` ist der Socket-Test rot (`[]`); mit beidem grün in zwölf Wiederholungen.
+- **Nachtrag (Branch-Review 2026-10-20, Fund JF-4):** `MemoryManagementTest` (laut Javadoc der Wächter für den Kontext-Shift) prüfte keinen: die drei Shift-Tests verlangten nur eine nicht leere Ausgabe, und mit `setCtxSize(32)` gab es nichts zu verschieben, weil llama.cpp `n_ctx` auf ein Vielfaches von 256 aufrundet (`llama-context.cpp:377`, b11639) – 21 Prompt- plus 25 erzeugte Token passten in den Slot (gemessen: alle 25 erzeugt, ohne `--context-shift`). Dazu fehlte das seit b11639 nötige `--context-shift`. Jetzt: das kleine Entwurfsmodell (die Tests erzeugen je 300 Token), Kontext 256, ein Slot, `enableContextShift()`, und der Helfer `assertGeneratedPastTheContext` verlangt `completion_tokens` = 300 und Prompt plus Antwort über 256 (über `completeWithStats`). Gegenprobe ohne `enableContextShift()`: alle drei rot (`every requested token must be generated, which needs a context shift: … expected: <300> but was: <107>`, `truncated: true`, `stop_type: limit`; das Winzmodell kappt den Slot zusätzlich auf seinen Trainingskontext 128); mit dem Shift grün (10/10, dreimal). Javadoc und Kommentare beschreiben den Shift jetzt in `pre_decode()` (server-context.cpp) statt im früheren `server.hpp`. Nur Test.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `4fb82c1`
+
+
+### F187 – java-llama.cpp: `addLoraScaledAdapter`/`addControlVectorScaled` senden `FNAME,SCALE`, upstream liest `FNAME:SCALE`
+- **Zeit:** 2026-10-19 23:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/ModelParameters.java` (`addLoraScaledAdapter`, `addControlVectorScaled`, neu `scaledField`), `llama/src/test/java/net/ladenthin/llama/parameters/ModelParametersTest.java`
+- **Schwere:** mittel (das Modell lädt überhaupt nicht; die Ausnahme nennt die Ursache nicht)
+- **Historie:** Gefunden beim Messauftrag `BUGHUNT-MEASUREMENTS.md`, M6: der erste Versuch, die Referenz mit Adapter über `addLoraScaledAdapter(ADAPTER, 1.0f)` zu laden, scheiterte mit `LlamaException: Failed to parse model parameters`, worauf die Messung auf `--lora` auswich. Upstream hat das Format mit `487674fbb` (b7448, „common: fix --override-kv to support comma-separated values“, #18056, 2025-12-17) umgestellt und mit `07fbe19f1` (b7648, #18643) um CSV-Maskierung erweitert; das Komma ist seither der **Listen**trenner, der Feldtrenner ist ein Doppelpunkt. Der Pin ist b11639, die Methode war also rund 4200 Builds tot.
+- **Beschreibung:** Beide Methoden rendern `putString(…, fname + "," + scale)`. `common/arg.cpp` am Pin liest den Wert als `FNAME:SCALE,...`: `parse_csv_row(value)`, dann je Element `string_split(item, ':')` mit der Bedingung `parts.size() != 2 → throw`. `--lora-scaled ,1.0` scheitert daher im argv-Parser mit `invalid stof argument`, und der Aufrufer sieht nur „Failed to parse model parameters“. Gemessen gegen den echten Parser (je ein Kind-JVM über `ServerLauncher` → `NativeServer`, der llama-server-argv wörtlich weitergibt): Form `,1.0` → `invalid stof argument`, Server startet nicht; `:1.0` → `lora-scaled format: FNAME:SCALE`, Server startet nicht; `:1.0` → angenommen, Server lauscht; `--lora ` → angenommen. Abgedeckt war die Methode nur von `ModelParametersTest#testAddLoraScaledAdapter`, das genau das defekte Format festschrieb (`is("adapter.bin,0.5")`) — die Klasse Fehler, die CLAUDE.md für die Wire-Name-Registries beschreibt: ein Test über die String-Abbildung bleibt grün, während das Flag tot ist. `test_model_flags.cpp` sieht es nicht, es hält den **Namen** gegen die Optionstabelle des Servers, nicht das **Werteformat**.
+- **Begleitende upstream-Grenze, nicht downstream behebbar:** `string_split` trennt an *jedem* Doppelpunkt (`common/common.h`), also ergibt `X:\models\a.gguf:1.0` drei Teile statt zwei. **Über `--lora-scaled` lässt sich kein absoluter Windows-Pfad ausdrücken**, auch im korrekten Format nicht (Form 2 oben). Ein reiner `,`→`:`-Tausch hätte auf Windows weiter nicht geladen.
+- **Fix:** Die beiden Methoden rendern `FNAME:SCALE` über das neue private `scaledField`, das einen Pfad mit Doppelpunkt mit `IllegalArgumentException` ablehnt — die Meldung nennt das Flag, das Format, den Pfad und die zwei Auswege (relativer Pfad, oder ungeskaliert laden und die Skala nach dem Laden mit `LlamaModel.setLoraAdapter` setzen). Eine laute Ablehnung beim Bauen ist besser als ein Ladefehler ohne Ursache; die Grenze ist im Javadoc von `addLoraScaledAdapter` erklärt, auf das `addControlVectorScaled` verweist. Beide gehen jetzt durch `appendCsvValue` (siehe F188).
+- **Tests:** `ModelParametersTest#testAddLoraScaledAdapter` und `#testAddControlVectorScaled` auf `FNAME:SCALE` umgestellt; neu `#aScaledPathContainingAColonIsRefusedWithTheReason` (beide Optionen, Meldung nennt Flag, Format und Pfad, relativer Pfad als Gegenprobe im selben Test).
+- **Rot-Grün-Nachweis:** Vorher scheiterte `new LlamaModel(base().addLoraScaledAdapter(adapter, 1f))` mit `Failed to parse model parameters`. Nachher, end-to-end mit dem Adapterpaar aus M6 (`ggml-org/stories15M_MOE`, Wegwerf-Probe, danach entfernt): `--lora-scaled :1.0` lädt und liefert denselben Text wie die `--lora`-Referenz, `:0.0` denselben wie ohne Adapter, und der Server meldet `"scale":0.0` zurück — die Skala wirkt also, was über `ModelParameters` vorher gar nicht möglich war. Danach alle Module grün: Kern 2154/0/0/18 (2136 ausgeführt), `ctest` 643/643, SpotBugs 0, Agent 281/0/0/60, `llama-ffm` 30/0/0/0, `llama-langchain4j` 55/0/0/7, `llama-kotlin` 6/0/0/0.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `5fff6b8`
+
+
+### F188 – java-llama.cpp: Die vier „can be repeated“-Adapteroptionen ersetzen den vorigen Wert statt ihn zu ergänzen
+- **Zeit:** 2026-10-19 23:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/CliParameters.java` (neu `appendCsvValue`), `ModelParameters.java` (`addLoraAdapter`, `addLoraScaledAdapter`, `addControlVector`, `addControlVectorScaled`)
+- **Schwere:** niedrig bis mittel (von mehreren Adaptern wirkt nur der letzte, ohne Hinweis)
+- **Historie:** Gefunden beim Beheben von F187 — dieselben vier Methoden, dieselbe Wire-Option, aber ein eigener Fehler.
+- **Beschreibung:** Alle vier versprechen im Javadoc „can be repeated to use multiple adapters“ bzw. „can be repeated to add multiple control vectors“. Der Parametersatz ist aber eine `LinkedHashMap`, die nach dem Wire-String schlüsselt, und `putString` ist `parameters.put(option, value)` — ein zweiter Aufruf überschreibt den ersten still. Für einen Setter ist das richtig, für ein `addX` nicht. Upstream nimmt genau diese Optionen seit #18056/#18643 als **einen** komma-separierten Wert, Ergänzen ist also das, was sie zusammensetzt.
+- **Fix:** Neues `protected final appendCsvValue(ModelOption, String)` in `CliParameters`: hängt an einen vorhandenen Wert mit Komma an und schreibt jeden Wert als CSV-Feld — in Anführungszeichen mit verdoppelten inneren Anführungszeichen, sobald er ein Komma oder ein Anführungszeichen trägt. Das ist nötig, weil upstreams `parse_csv_row` ein öffnendes Anführungszeichen nur am **Feldanfang** erkennt, ein Pfad mit Komma also anders nicht übergeben werden kann. Die vier Methoden nutzen es; `setOverrideKv` bleibt unverändert, es ist ein Setter, der den Wert des Aufrufers durchgibt.
+- **Tests:** `ModelParametersTest#theRepeatableAdapterOptionsAccumulateInsteadOfReplacing` (alle vier Optionen, je zwei Aufrufe, mit den erwarteten Listenwerten einschließlich `c.gguf:0.5,d.gguf:1.0`) und `#aValueCarryingACommaOrAQuoteIsWrittenAsAQuotedCsvField` (Komma, Anführungszeichen, und ein einfacher Wert vor einem zu maskierenden).
+- **Rot-Grün-Nachweis:** Die Akkumulationszusicherungen sind gegen `putString` rot (der erste Wert fehlt). Die End-to-end-Probe aus F187 zeigt zwei skalierte Adapter als einen Wert: `--lora-scaled ..\models\moe_shakespeare15M.gguf:0.5,second.gguf:0.25`. Modullaufzahlen wie in F187.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `5fff6b8`
+- **Nachtrag (Branch-Review 2026-10-20):** Der `isEmpty()`-Zweig in `appendCsvValue` ließ einen leeren Vorwert still verschwinden; behoben als F234 (ein leerer Wert wird abgewiesen).
+
+
+### F189 – java-llama.cpp: Ohne `--model-id` nennen `/v1/models` und die Antworten dasselbe Modell verschieden
+- **Zeit:** 2026-10-19 23:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiServerCli.java` (`toModelParameters`), `OpenAiServerConfig.java` (Javadoc von `getModelId`), `llama/src/test/java/net/ladenthin/llama/server/OpenAiServerCliTest.java`
+- **Schwere:** niedrig bis mittel (ein Client, der den aus `/v1/models` gelesenen Namen gegen das `model`-Feld der Antwort hält, sieht zwei verschiedene; zusätzlich steht ein absoluter lokaler Pfad in jeder Antwort, auch über das Netz)
+- **Historie:** Gefunden beim Messauftrag `BUGHUNT-MEASUREMENTS.md`, M5(b). F185 hatte die Hälfte erwischt: sein Test hieß `anExplicitModelIdBecomesTheModelsAlias` und sein Kommentar beschrieb genau diesen Fehler für den **expliziten** Fall, schrieb für den Vorgabefall aber fest, dass *kein* `--alias` gesetzt wird.
+- **Beschreibung:** `OpenAiServerCli.ParsedArgs.getModelId()` leitet ohne `--model-id` den Dateinamen ab, und `/v1/models` meldet ihn. Das `model`-Feld einer Antwort kommt dagegen nicht aus der Konfiguration, sondern aus llama.cpp, und `toModelParameters()` setzte `--alias` nur unter `if (modelId != null)`. Ohne Alias ist upstreams Kette für den Modellnamen (`tools/server/server-context.cpp:1531`) Alias → `common_params_model::get_name()` → Dateiname, und `get_name()` (`common/common.h:312`) gibt für eine lokale Datei `path` zurück — der Dateiname-Zweig, den upstreams eigener Kommentar „fallback: derive model name from file name“ beschreibt, ist für `-m ` also unerreichbar. Gemessen mit zwei Kind-JVMs: ohne `--model-id` meldete `/v1/models` `AMD-Llama-135m-code.Q2_K.gguf`, jede Antwort und jeder Stream-Chunk den vollen Pfad; mit `--model-id my-model` stimmten beide; `NativeServer --alias my-model` als Gegenprobe ebenfalls. Die `id`-Hälfte von F185 ist nicht betroffen (`chatcmpl-`, eine Id pro Stream, in allen drei Fällen).
+- **Fix:** `toModelParameters()` setzt den Alias **immer**, aus `getModelId()` statt aus `modelId`, damit der Vorgabewert dieselbe Zusage trägt wie das Flag — die Zusage, die der Kommentar an der Stelle schon aussprach („this keeps them equal to /v1/models“).
+- **Bewusst nicht geändert:** der eingebettete Weg. `OpenAiCompatServer(LlamaModel, OpenAiServerConfig)` bekommt ein schon geladenes Modell und kann kein `--alias` mehr setzen, also trägt eine Antwort dort weiter den Namen, mit dem das Modell geladen wurde (beobachtet in M2, wo die Probe `modelId("m")` setzt und die Antworten den Pfad nennen). Das steht jetzt im Javadoc von `OpenAiServerConfig.getModelId()` mit dem Hinweis, dass ein einbettender Aufrufer sein Modell mit `ModelParameters.setAlias(…)` laden muss. Das `model`-Feld einer Antwort nachträglich zu überschreiben wäre die Alternative; sie würde über die Engine etwas behaupten, was diese nicht getan hat, und `NativeServer` im Attach-Modus könnte es ohnehin nicht nachvollziehen.
+- **Tests:** `OpenAiServerCliTest#theAdvertisedModelIdBecomesTheModelsAliasWithAndWithoutTheFlag` — ersetzt `anExplicitModelIdBecomesTheModelsAlias`, prüft beide Fälle und dass der Alias und `getModelId()` derselbe String sind.
+- **Rot-Grün-Nachweis:** Der alte Test schrieb das Gegenteil fest und ist gegen den Fix rot. End-to-end (Wegwerf-Probe, danach entfernt), Server ohne `--model-id` über `ServerLauncher`: `/v1/models`, die blockierende Antwort und jeder Stream-Chunk nennen alle `AMD-Llama-135m-code.Q2_K.gguf`, kein absoluter Pfad erreicht eine Antwort, `id` ist `chatcmpl-…` und je Stream eine. Modullaufzahlen wie in F187.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `5fff6b8`
+
+
+### F190 – java-llama.cpp: Das Skript-Backend des Agenten schreibt unmaskierte Steuerzeichen in seine JSON-Chunks (Modul baut auf Windows nicht grün)
+- **Zeit:** 2026-10-19 22:35 UTC
+- **Datei:** `llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/ScriptedBackend.java` (`quote`), neu `ScriptedBackendTest.java`
+- **Schwere:** niedrig (reiner Testcode, nichts davon wird ausgeliefert — aber das Agentenmodul ist auf Windows nicht baubar)
+- **Historie:** Gefunden beim Messauftrag `BUGHUNT-MEASUREMENTS.md`, M8: der modellfreie Lauf des Agentenmoduls war rot, deterministisch in 3 von 3 Läufen. Die drei Agenten-Jobs laufen nur auf Linux, wo `System.lineSeparator()` ein einzelnes LF ist, darum hat CI es nie gesehen.
+- **Beschreibung:** `quote` maskierte nur `"`, `\` und `\n` und schrieb alles andere unverändert durch. RFC 8259 verbietet U+0000..U+001F unmaskiert in einem String, also war der Chunk kein JSON, sobald der Text ein Steuerzeichen trug. Gemessen mit beiden `quote`-Fassungen gegen Jackson: **31 von 32** Steuerzeichen ergaben ungültiges JSON, nur `\n` war behandelt; der Wert selbst wurde in keiner Fassung verändert, es ging rein um Gültigkeit. `AgentSessionTest#aLoopRunsItsStepsThroughTheFrontEndUntilTheMarker` ist der einzige Test, der `System.lineSeparator()` einspeist: auf Windows landete ein rohes CR im JSON-String (`Illegal unquoted character ((CTRL-CHAR, code 13))`), der Client verwarf den Strom (`HTTP/1.1 header parser received no bytes`), jeder Loop-Schritt bekam nichts, und der Stillstandswächter beendete den Loop nach drei Schritten. Was rot wurde, war also eine Zusicherung über die **Antwort**, mehrere Schichten von der Ursache entfernt. `TaskLoop.isComplete` ist für beide Zeilenenden `true`, die Sentinel-Erkennung ist nicht beteiligt.
+- **Fix:** `quote` bekommt `\r`, `\t`, `\b`, `\f` und — das Tragende — einen `default`-Zweig, der alles unter `0x20` als `\uXXXX` schreibt, damit ein nicht aufgelistetes Steuerzeichen die Lücke nicht wieder aufreißen kann. Der Test selbst bleibt unangetastet: `System.lineSeparator()` dort ist richtig, weil genau das CRLF überhaupt erst ausübt. Ein `grep` über alle handgerollten JSON-Quote-Helfer im Repository zeigt, dass es diese eine Stelle war.
+- **Tests:** Neu `ScriptedBackendTest` (3): jeder Chunk eines Text-Zuges parst als JSON (mit `System.lineSeparator()`, CRLF, LF und einzelnem CR); **jedes** der 32 Steuerzeichen einzeln, also der `default`-Zweig und nicht die vier benannten Fälle; und der Text kommt unverändert aus dem Chunk zurück.
+- **Rot-Grün-Nachweis:** Mit der alten `quote`-Fassung kurz zurückgesetzt: `ScriptedBackendTest` 2 Failures + 1 Error (`System.lineSeparator(): chunk is not JSON`, `U+0000: chunk is not JSON`, `JsonParse Illegal unquoted character ((CTRL-CHAR, code 13))`) und `AgentSessionTest` 1 Failure. Mit dem Fix (Datei danach byte-identisch wiederhergestellt) beide dreimal hintereinander grün, und das ganze Modul 281/0/0/60 — die 60 sind die dokumentierten JLine-Fälle. `spotless:check` grün.
+- **Nachtrag (Branch-Review 2026-10-20):** `#everyControlCharacterSurvivesAsJson` prüfte für die 32 Steuerzeichen nur, dass jeder Chunk parst; jedes `\uXXXX` und jede kurze Maske parst aber, so dass ein falsches Zeichen durchging – auf Linux ungeprüft blieben die Werte von `\r`, `\b` und `\f` (Fund J15-2). Der neue Helfer `contentOf(List)` hängt das `delta.content` aller Chunks eines Zuges aneinander; der Test vergleicht damit jetzt für jedes Zeichen von U+0000 bis U+001F sowie für Anführungszeichen/Backslash und DEL/U+0085/U+2028 den Text mit der Eingabe, und `#theTextComesBackOutOfTheChunkUnchanged` nutzt denselben Helfer statt der eigenen Schleife. Gegenprobe rot: `\r` als `\n` geschrieben (`U+000D Expected: is "a\rb" but: was "a\nb"`; die alte Testfassung blieb damit auf Linux grün, 3/3), `\b` als `\f` (`U+0008`); unverändert 3/3 grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `d68aa3d`
+
+
+### F197 – java-llama.cpp: `n` > 1 wird auf jedem nativen Completion-Weg still auf eine Antwort reduziert (das gestreamte `/v1/completions` verlor dabei den Schutz aus F180)
+- **Zeit:** 2026-10-20 10:30 UTC
+- **Datei:** `llama/src/main/cpp/jllama_engine.cpp` (`populate_completion_task`), `llama/src/test/cpp/test_c_api.cpp`, `llama/src/test/java/net/ladenthin/llama/server/OpenAiServerCompletionIntegrationTest.java`
+- **Schwere:** mittel (zwei Antworten verlangt, eine geliefert, ohne Hinweis — dieselbe Klasse wie F184)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20. F180 hatte `n > 1` im Java-Mapper für den gestreamten `/v1/completions` abgelehnt; F186 (`4fb82c1`) verlegte diesen Weg auf den nativen OAI-Pfad und entfernte `toCompletionParameters` und damit die Ablehnung samt ihrer Testzusicherung.
+- **Beschreibung:** Upstreams Schema liest `n` als `n_cmpl` mit den harten Grenzen 1 … `n_parallel` (`server-schema.cpp:62`) und beantwortet die weiteren Completions in Kind-Tasks, die der Server anlegt (`server-context.cpp:4888`). Die Engine stellt genau einen Task ein und legt keine Kinder an. Jedes `n` zwischen 2 und der Slot-Zahl wurde also angenommen und mit einer Wahl beantwortet — gemessen vom Reviewer mit dem Zufallsmodell und `setParallel(2)`: `handleCompletionsOai` mit `n:2` eine Wahl, `streamCompletionsOai` ebenso; erst `n:3` scheiterte am Schema. Betroffen: C-API, FFM, `LlamaModel.handle*`, das blockierende `/v1/completions` (leitet den Körper wörtlich weiter) und seit F186 das gestreamte.
+- **Fix:** `populate_completion_task` lehnt `task.params.n_cmpl > 1` nach dem Schema mit `jllama_invalid_request` ab ("a request takes one completion, this one asks for 'n' = N; send separate requests"), wie mehrere Prompts (F184). Das deckt die blockierenden, gestreamten und Chat-Wege an einer Stelle.
+- **Tests:** `CApi.SeveralCompletionsInOneRequestAreRefusedNotDropped` (eigenes Modell mit `-np 2`; `jllama_completions_oai`, `jllama_completions` mit `n_cmpl`, der Stream-Request und `jllama_chat_completions` werden abgelehnt, `n:1` liefert eine Wahl), `OpenAiServerCompletionIntegrationTest#aCompletionAskingForSeveralChoicesIsAnswered400OnBothRoutes` (beide `/v1/completions`-Routen, das Testmodell jetzt mit `setParallel(2)`), `#severalChoicesAreRefusedOnTheModelsOwnCompletionAndChatPaths`.
+- **Rot-Grün-Nachweis:** Der C-API-Test gegen den alten Code rot (alle vier Wege `OK` statt `INVALID_REQUEST`), mit dem Fix grün; `ctest` grün. Die beiden Java-Tests lokal mit dem Zufallsmodell grün (die zwei Infill-Tests der Klasse scheitern dort erwartungsgemäß, das Zufallsmodell hat keine FIM-Tokens); in CI mit dem echten Modell.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `7a51bbc`
+- **Nachtrag (Branch-Review 2026-10-20, Fund XJ-5):** Die Ablehnung mehrerer Prompts (F184) und von `"n"` > 1 stand im C-Header nur bei `jllama_completions`/`jllama_completions_oai` und in der Javadoc von `streamCompletionsOai` nur zur Hälfte. Jetzt nennen sie die `@throws InvalidRequestException` von `LlamaModel#handleCompletions`, `#handleCompletionsOai` und `#streamCompletionsOai` (beides), `#handleChatCompletions` und `#handleInfill` (nur `"n"`: eine Chat-Anfrage rendert genau einen Prompt aus `messages`, Infill baut ihn selbst), alle acht Anfrage-Methoden von `FfmLlamaModel` mit derselben Aufteilung, und im C-Header die Chat-, Infill- und Stream-Funktionen. Formuliert als unbedingte Ablehnung (ein Prompt und eine Completion je Anfrage), nicht über die Slot-Zahl. Nur Doku.
+
+
+### F198 – java-llama.cpp: `FfmTextToSpeech.close()` gibt die Engine frei, während `synthesize()` auf einem anderen Thread in ihr läuft (FFM-Hälfte von F16)
+- **Zeit:** 2026-10-20 11:00 UTC
+- **Datei:** `llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmTextToSpeech.java`, neu `llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTextToSpeechTest.java`
+- **Schwere:** mittel (Use-after-free → JVM-Absturz, nur bei `close()` parallel zu einer laufenden Synthese)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20: F16 nennt die Datei als betroffen, auf dem Branch war sie aber unverändert, und F16 stand auf „gefixt“.
+- **Beschreibung:** `synthesize()` prüfte `closed.get()` und rief dann ohne Lock `jllama_tts_synthesize`; `close()` setzte das Flag per CAS und rief `jllama_tts_free` → `engine_free` → `delete engine`. Die native Seite zählt keine Nutzer (`jllama_api.cpp`), also löschte ein `close()` von einem anderen Thread die Engine samt ihrem `synthesize_mutex` unter einer laufenden Synthese — dieselbe Prüfen-dann-Rufen-Lücke wie F59 bei `FfmNativeServer`.
+- **Fix:** Ein `ReentrantReadWriteLock` wie im JNI-`TextToSpeech`: Synthesen halten das Lese-Lock (die Engine serialisiert sie selbst), `close()` nimmt das Schreib-Lock und wartet so auf jede laufende Synthese; das `closed`-Flag steht unter dem Lock. Die zwei nativen Aufrufe liegen hinter einem `Backend`-Seam, wie bei `FfmNativeServer`, damit die Sperre ohne Bibliothek testbar ist.
+- **Tests:** `FfmTextToSpeechTest` (5, ohne Bibliothek und Modell): `closeWaitsForASynthesisInsideTheNativeCall`, `synthesizeAfterCloseIsRefusedWithoutTouchingTheHandle`, `closeTwiceFreesOnce`, `synthesizeReturnsTheBackendsWav`, `synthesizeWithoutTextIsRefused`.
+- **Rot-Grün-Nachweis:** Mit dem Seam, aber ohne Lock: `closeWaitsForASynthesisInsideTheNativeCall` rot (`close()` kehrte zurück, während die Synthese noch lief); mit dem Lock grün. `llama-ffm` `verify` 35/35 mit dem Zufallsmodell.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `c07aad0`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J13-1):** `llama-ffm/README.md` nannte noch „7 model-free tests“ (Stand vor dem Branch) und behauptete, die nativen Tests übersprängen sich ohne Bibliothek; der CLAUDE.md-Punkt „Tests“ des Moduls kannte nur `JllamaNativeTest` und `FfmLlamaModelIntegrationTest`. Das README nennt jetzt keine Zahl mehr und sagt, welche Klassen ohne Bibliothek laufen (`FfmNativeServerTest`, `FfmTextToSpeechTest`, über Fake-`Backend`s, F59/F198), welche sich überspringen (`JllamaNativeTest`, der modellgestützte Test) und dass `FfmRpcServerTest` eine braucht (er überspringt sich nicht); CLAUDE.md zählt die beiden Lebenszyklus-Klassen, die drei `takeStrings`-Fälle, `FfmRpcServerTest`, `oaiCompletionStreamEndsWithUsage` und die beiden Callback-Fehlerfälle auf. Nur Doku.
+
+
+### F199 – java-llama.cpp: Ein Heartbeat vor dem Eröffnungsevent verschluckt `message_start` (Anthropic) bzw. `response.created` (Responses)
+- **Zeit:** 2026-10-20 11:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java` (`ResponseStream`, `streamAnthropic`, `streamResponses`), neu `llama/src/test/java/net/ladenthin/llama/server/ResponseStreamTest.java`
+- **Schwere:** mittel (Client erhält einen Stream ohne Eröffnungsevent; Anthropic-SDKs verwerfen ihn)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20.
+- **Beschreibung:** Beide Routen planten den Heartbeat-Timer **vor** `out.startWith(translator.begin())`. Feuerte der Heartbeat dazwischen (oder kam die Antwort erst nach einem Heartbeat in Gang), startete er den Stream mit leerem Prelude; `startWith` setzte danach nur noch das Feld, das `body()` nur beim Start schreibt — das Eröffnungsevent ging still verloren.
+- **Fix:** `startWith` schreibt den Text sofort, wenn der Stream schon läuft (ein Heartbeat ist ein SSE-Kommentar, kein Event, also bleibt das Eröffnungsevent das erste Event); beide Routen setzen die Eröffnung zudem vor dem Planen des Heartbeats.
+- **Tests:** `ResponseStreamTest` (4, über einen `HttpExchange`-Fake ohne Socket): `theOpeningIsWrittenFirstWhenItIsSetBeforeTheFirstWrite`, `anOpeningSetAfterAHeartbeatStartedTheStreamIsStillWritten`, `anErrorBeforeTheFirstWriteIsAnsweredWithItsStatusAndNoOpening`, `anErrorAfterTheFirstWriteIsLeftToTheStream`.
+- **Rot-Grün-Nachweis:** Ohne den Fix `anOpeningSetAfterAHeartbeatStartedTheStreamIsStillWritten` rot (erwartet `: ping\nOPEN\nDATA\n`, war `: ping\nDATA\n`); mit ihm grün. `OpenAiCompatServerHttpTest` 63/63.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `aa58171`
+
+
+### F200 – java-llama.cpp: `chatBatch` startet einen Thread pro Anfrage ohne Obergrenze, und ein `Error` beendet das Warten vor den anderen Anfragen
+- **Zeit:** 2026-10-20 11:45 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/BatchDispatch.java`, `llama/src/main/java/net/ladenthin/llama/LlamaModel.java` (`chatBatch`), `llama/src/test/java/net/ladenthin/llama/BatchDispatchTest.java`
+- **Schwere:** niedrig bis mittel (eine große Liste wird zu ebenso vielen Threads; nach einem `Error` kehrt der Aufruf zurück, während Anfragen noch laufen)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20; `runEachOnItsOwnThread` entstand mit dem Fix, der `chatBatch` vom gemeinsamen `ForkJoinPool` löste.
+- **Beschreibung:** `Executors.newFixedThreadPool(requests.size(), ...)`: zehntausend Anfragen waren zehntausend Threads, obwohl der native Scheduler nur so viele gleichzeitig rechnet, wie er Slots hat. Und `asRuntimeException` warf einen `Error` aus der ersten Future, die ihn trug, sofort — die übrigen wurden nicht mehr abgewartet, entgegen der Zusage, dass ein Fehler erst nach allen Anfragen propagiert.
+- **Fix:** `runEachOnItsOwnThread(requests, maxThreads, call)`: der Pool hat `min(requests.size(), maxThreads)` Threads (`chatBatch` übergibt `MAX_CHAT_THREADS` = 64), weitere Anfragen starten, sobald ein Thread frei wird; eine Grenze unter 1 wird abgelehnt. Der erste Fehler jeder Art wird gemerkt und erst nach allen Futures geworfen.
+- **Tests:** `BatchDispatchTest`: neu `runEachOnItsOwnThread_runsNoMoreRequestsAtOnceThanTheBound` (12 Anfragen, Grenze 3, höchstens 3 gleichzeitig), `runEachOnItsOwnThread_anErrorIsThrownAfterTheOthersFinished`, `runEachOnItsOwnThread_aBoundBelowOneIsRefused`.
+- **Rot-Grün-Nachweis:** Mit der neuen Signatur, aber ohne Grenze und ohne den Error-Pfad: die drei neuen Tests rot (12 gleichzeitig statt 3; zwei statt null fertige Anfragen fehlten beim Wurf; keine Ablehnung von 0); mit dem Fix 11/11 grün.
+- **Nachtrag (Branch-Review 2026-10-20):** `runEachOnItsOwnThread_runsNoMoreRequestsAtOnceThanTheBound` behauptete „genau 3 gleichzeitig“ aus 20-ms-Sleeps; das „genau“ hing an der Planung der Threads (Fund J9-2). Jetzt mit zwei Latches statt Sleep: jede Anfrage wartet, bis drei drin sind (das Maximum erreicht die Grenze ohne Zeitbezug), dann bis zu 200 ms auf eine vierte, die nur ein Pool über der Grenze starten kann. Gegenprobe ohne die Grenze (`newFixedThreadPool(requests.size())`) deterministisch rot (`Expected: is <3> but: was <4>`); mit ihr 18/18 in der Klasse grün, dreimal wiederholt.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `a2bfad8`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J9-6):** Die Begründung der `HES_LOCAL_EXECUTOR_SERVICE`-Ausnahme in `llama/spotbugs-exclude.xml` beschrieb noch einen Thread pro Anfrage, alle zugleich; sie nennt jetzt `min(Batchgröße, maxThreads)` mit `MAX_CHAT_THREADS` = 64 für `chatBatch` und das Herunterfahren im `finally`. Abschnitt 2.4 von `docs/feature-investigation-llama-stack-client-kotlin.md` sprach noch von den asynchronen Wrappern; er nennt jetzt beide Mechanismen (`completeBatch`/`completeBatchWithStats` reichen auf dem aufrufenden Thread ein und empfangen in Eingabereihenfolge, F167; `chatBatch` über den Pool mit höchstens 64 Threads, F200). Die Ausnahme selbst bleibt. Nur Doku.
+
+
+### F201 – java-llama.cpp: `--model-id` mit Komma (oder abgeleitet aus einem Dateinamen mit Komma) wird zu mehreren llama.cpp-Aliasen; `/v1/models` und die Antworten nennen das Modell verschieden
+- **Zeit:** 2026-10-20 12:00 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiServerCli.java`, `llama/src/test/java/net/ladenthin/llama/server/OpenAiServerCliTest.java`
+- **Schwere:** niedrig (falscher Modellname in den Antworten; Clients, die den Namen aus `/v1/models` vergleichen, verwerfen sie)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20; Folge von F185, das die ID immer als `--alias` weitergibt.
+- **Beschreibung:** Upstreams `--alias` (`common/arg.cpp`, b11639) teilt den Wert an Kommas, strippt jeden Teil und legt ihn in ein `std::set`; die Antworten nennen den ersten Eintrag. `--model-id my,model` meldete in `/v1/models` `my,model`, in jeder Antwort `model`; dasselbe für eine aus `a,b.gguf` abgeleitete ID, und eine ID aus Leerzeichen wurde ganz verworfen (die Antworten nannten wieder den Pfad).
+- **Fix:** `--model-id`/`--model-alias` lehnen ein Komma, einen leeren Wert und Leerraum am Rand mit einer Nutzungsmeldung ab; die aus dem Dateinamen abgeleitete ID ersetzt Kommas durch `_` und wird getrimmt (leer → Default-ID), also genau die Form, die der Alias behält.
+- **Tests:** `OpenAiServerCliTest`: `aModelIdWithACommaIsRefused`, `aModelIdLlamaCppWouldStripIsRefused`, `aFileNameWithACommaIsAdvertisedAsItsAlias`.
+- **Rot-Grün-Nachweis:** Ohne den Fix alle drei rot (keine Ablehnung; abgeleitete ID `a,b.gguf`); mit ihm 54/54 grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `8e2642f`
+
+
+### F202 – java-llama.cpp: Die Anthropic-Route holt für das Stoppwort upstreams ganzes Debug-Objekt samt gerendertem Prompt durch JNI
+- **Zeit:** 2026-10-20 12:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java`, `OpenAiRequestMapper.java`, `parameters/InferenceParameters.java` (neu `withResponseFields`), `parameters/RequestField.java` (`RESPONSE_FIELDS`), `parameters/ParameterJsonSerializer.java`
+- **Schwere:** niedrig (Aufwand: jede Anthropic-Antwort trug den vollständigen Prompt, die Token-Liste und die Generierungseinstellungen zusätzlich, um sie danach zu verwerfen)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20; Folge von F183, das `verbose` setzt, um `stop_sequence` zu füllen.
+- **Beschreibung:** `verbose: true` hängt an die OpenAI-Antwort `__verbose` = `to_json_non_oaicompat()` des Endergebnisses: Inhalt, Tokens, `prompt` (der ganze gerenderte Prompt), `generation_settings`, `timings` … Gebraucht wird nur `stopping_word`. Upstreams `response_fields` projiziert genau dieses Objekt (`json_get_nested_values`), war aber weder ein `RequestField` noch vom Mapper durchgereicht.
+- **Fix:** `RequestField.RESPONSE_FIELDS` (SCHEMA-Vertrag, gegen `make_llama_cmpl_schema` geprüft) mit `InferenceParameters.withResponseFields(String...)`; der Mapper reicht `response_fields` durch und lehnt alles außer einem String-Array mit 400 ab; die Anthropic-Übersetzung setzt `response_fields: ["stopping_word"]`.
+- **Tests:** `AnthropicApiSupportTest.theTranslatedRequestAsksForTheStopWord` (erweitert), `OpenAiRequestMapperTest.responseFieldsAreForwarded`, `responseFieldsThatAreNotStringsAreRefused`, `InferenceParametersTest.withResponseFieldsWritesTheProjectionAsAStringArray`, modellgestützt `OpenAiCompatServerIntegrationTest.theDebugObjectTheAnthropicRouteAsksForCarriesOnlyTheStopWord` (`__verbose` hat genau den Schlüssel `stopping_word`); C++ `test_wire_contracts` prüft den neuen Schlüssel gegen upstreams Schema.
+- **Rot-Grün-Nachweis:** Mit `RequestField`/`withResponseFields`, aber dem alten Mapper und der alten Übersetzung: drei Tests rot; mit dem Fix grün (221 Tests der fünf Klassen, Integrationstest 13/13 mit dem Zufallsmodell, ctest Wire 6/6).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `29ff8c1`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J8-1, Testteil):** upstreams Projektion (`json_get_nested_values`, `server-common.cpp`, b11639) trennt verschachtelte Pfade mit `/`, nicht mit `.`; der Schlüssel der Projektion ist der ganze Pfad, ein unbekannter Pfad fällt ohne Fehler weg. `OpenAiCompatServerIntegrationTest#theDebugObjectTheAnthropicRouteAsksForCarriesOnlyTheStopWord` fragt nur den Schlüssel `stopping_word` ab und sieht den Unterschied nicht. `OpenAiCompatServerIntegrationTest#aNestedResponseFieldIsNamedWithASlashAndKeepsThePathAsItsKey` schickt `verbose: true` mit `response_fields: ["timings/predicted_n"]` (in `__verbose` genau dieser Schlüssel, ganzzahlig > 0) und mit `["timings.predicted_n"]` (`__verbose` leer). Mit der Punkt-Schreibweise in der ersten Anfrage rot („Expected: is <[timings/predicted_n]> but: was <[]>“); lokal mit dem Winzmodell grün (14/14 der Klasse). Das Beispiel in `InferenceParametersTest#withResponseFieldsWritesTheProjectionAsAStringArray` heißt jetzt `timings/predicted_n`; die Javadoc von `withResponseFields` und die CHANGELOG-Zeile folgen in der Doku-Phase.
+- **Nachtrag (Branch-Review 2026-10-20, Fund J8-1, Doku-Teil):** Die Javadoc von `InferenceParameters#withResponseFields` nennt jetzt Pfade mit `/` (`timings/predicted_n`, wie `json_get_nested_values` sie trennt), den ganzen Pfad als Schlüssel der Projektion (`{"timings/predicted_n": 12}`, kein verschachteltes `timings`) und dass ein Pfad, den das Ergebnis nicht hat -- ein gepunktetes `timings.predicted_n` ist so einer --, ohne Fehler wegfällt; die CHANGELOG-Zeile sagt dasselbe. Nur Doku.
+
+
+### F203 – java-llama.cpp: `chat_template_kwargs`: ein Wert mit mehr als einem JSON-Wert wird still auf den ersten gekürzt
+- **Zeit:** 2026-10-20 12:35 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/ParameterJsonSerializer.java` (`buildRawValueObject`; betrifft `InferenceParameters.withChatTemplateKwargs` und `ModelParameters.setChatTemplateKwargs`)
+- **Schwere:** niedrig (falscher Template-Parameter ohne Meldung)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20; dieselbe Klasse wie die `FAIL_ON_TRAILING_TOKENS`-Fixes in `JsonParameters` und `ChatRequest`, hier übersehen.
+- **Beschreibung:** `OBJECT_MAPPER.readTree(value)` liest den ersten JSON-Wert und ignoriert den Rest: `"true false"` wurde `true`, `{"a":1} {"b":2}` wurde `{"a":1}`. Die dokumentierte Regel (kein JSON → als String-Literal) griff dadurch nicht.
+- **Fix:** Ein `ObjectReader` mit `FAIL_ON_TRAILING_TOKENS`; ein Wert, der nicht genau ein JSON-Wert ist, wird wie dokumentiert zum String-Literal.
+- **Tests:** `ParameterJsonSerializerTest.testBuildRawValueObject_trailingTokensFallBackToString`, `InferenceParametersTest.aChatTemplateKwargHoldingTwoValuesIsKeptAsText` (beide öffentlichen Setter).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot (`{"two":true,…,"object":{"a":1}}`); mit ihm grün (40 + 114).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `af84cb5`
+
+
+### F204 – java-llama.cpp: `throw_utf8_impl`/`new_string_utf8_impl` rufen JNI über einer schon anstehenden Java-Exception auf und ersetzen sie
+- **Zeit:** 2026-10-20 12:50 UTC
+- **Datei:** `llama/src/main/cpp/jni_utf8.hpp`, `llama/src/test/cpp/test_jni_helpers.cpp`
+- **Schwere:** niedrig bis mittel (die genauere Exception geht verloren; unter Android-CheckJNI bricht ein `FindClass`/`NewObject` bei anstehender Exception den Prozess ab)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20; `jni_utf8.hpp` entstand mit dem Modified-UTF-8-Fix, `jni_guard_impl` hatte die Regel schon.
+- **Beschreibung:** Nach einem fehlgeschlagenen JNI-Aufruf (z. B. `GetStringUTFChars` mit `OutOfMemoryError`) rief ein Aufrufer `throw_utf8_impl`: der ASCII-Pfad `ThrowNew` ersetzte die anstehende Exception, der Nicht-ASCII-Pfad rief `FindClass`, `GetMethodID` und `NewObject` — laut JNI-Spezifikation bei anstehender Exception verboten. `jni_guard_impl` lässt eine anstehende Exception stehen, diese beiden Helfer nicht.
+- **Fix:** Beide prüfen zuerst `ExceptionCheck()`: `throw_utf8_impl` kehrt zurück und lässt die anstehende Exception stehen, `new_string_utf8_impl` gibt `nullptr` zurück (sein Vertrag: nullptr mit anstehender Exception).
+- **Tests:** C++ `JniUtf8.APendingExceptionIsLeftInPlaceOnBothPaths` (ASCII- und 4-Byte-Pfad, kein `ThrowNew`, kein String gebaut, nichts geworfen; `new_string_utf8_impl` ohne `NewStringUTF`).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot; mit ihm grün, ctest 645/645.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `da6fa8e`
+
+
+### F205 – java-llama.cpp: Ollama `/api/generate` ohne Prompt (das Vorladen eines Modells) erzeugt eine Antwort auf einen leeren Nutzer-Turn
+- **Zeit:** 2026-10-20 13:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java` (neu `isLoadOnly`, `toOllamaLoadResponse`), `OpenAiCompatServer.java` (`handleOllamaGenerate`)
+- **Schwere:** niedrig (unnötige Generierung bis `num_predict`/Kontextende bei jedem Vorladen; Clients erwarten `done_reason "load"`)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20; vorher erzeugte der Pfad ohne `raw` (Chat-Template seit diesem Branch) bzw. der `raw`-Pfad eine Antwort auf einen leeren Prompt.
+- **Beschreibung:** Ollama beantwortet ein `/api/generate` ohne Prompt mit dem Laden des Modells (`done: true`, leere `response`, `done_reason "load"`) — so laden Clients ein Modell vor. Hier lief ein leerer Nutzer-Turn durch das Chat-Template und wurde beantwortet.
+- **Fix:** Ohne Prompt und ohne `suffix` (ein leerer Prompt mit `suffix` bleibt FIM am Dateianfang) antwortet die Route sofort mit Ollamas Ladeantwort, ohne das Backend zu rufen — auch bei `stream` ein einzelnes Objekt, wie Ollama. Entladen (`keep_alive: 0`) gibt es hier nicht; das Modell bleibt geladen.
+- **Tests:** `OpenAiCompatServerHttpTest.ollamaGenerateWithoutAPromptOnlyReportsTheModelLoaded` (drei Formen, das Backend wird nicht gerufen), `OllamaApiSupportTest.aGenerateRequestWithoutAPromptOrSuffixOnlyLoadsTheModel`.
+- **Rot-Grün-Nachweis:** Ohne den Fix der HTTP-Test rot (das Backend generierte); mit ihm grün (64 + 37).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `200cc90`
+- **Nachtrag (Branch-Review 2026-10-20, Funde J1-4, J3-3, J16-2):** Beide Regeln sind bewusste Abweichungen von Ollama, nicht Ollamas Verhalten: Ollamas `GenerateHandler` prüft nur `req.Prompt == ""` (der `suffix` fügt dort nur `CapabilityInsert` hinzu), beantwortet also einen leeren Prompt mit `suffix` mit der Ladeantwort, und antwortet bei `keep_alive: 0` ohne Prompt mit `done_reason "unload"`. Hier bleibt der leere Prompt mit `suffix` FIM am Dateianfang (auch bei `"suffix":""`, `hasSuffix` fragt nur nach einem String), und `keep_alive: 0` bekommt die Ladeantwort, weil nichts entladen wird — ein „unload“ wäre eine Meldung über eine Aktion, die nicht stattfand. Die Javadoc von `isLoadOnly`/`toOllamaLoadResponse`, die README-Zeile der Ollama-Routen und `TODO.md` sagen das jetzt; vorher schrieb die Javadoc die `suffix`-Bedingung Ollama zu. Reine Doku, kein Test.
+- **Nachtrag (Branch-Review 2026-10-20, Fund J1-4, Verhaltenshälfte):** `isLoadOnly` las den Prompt mit `asText("")` und hielt damit auch einen `prompt`, der ein Array oder Objekt ist, für „kein Prompt“ — ein fehlerhafter Body bekam die Ladeantwort (200), wo Ollama beim Binden 400 antwortet. Behoben als [F244](#f244): nur ein fehlender, `null` oder leerer String-Prompt ist load-only, ein Prompt anderen Typs wird vorher mit 400 abgewiesen.
+
+
+### F213 – java-llama.cpp: Ollama `/api/chat` mit einem `messages`-Element, das kein Objekt ist, schließt die Verbindung ohne Antwort
+- **Zeit:** 2026-10-20 19:55 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java` (`toOpenAiChatRequest`), `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java` (`handleOllamaChat`)
+- **Schwere:** niedrig (eine Anfrage mit falscher Form bekommt statt 400 eine geschlossene Verbindung; sichtbar nur auf TRACE)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J1-1). Dieselbe Klasse wie F89 (`handleResponses` rief den Übersetzer außerhalb der Fehlerbehandlung auf); `/api/chat` war die letzte Ollama-Route mit diesem Aufbau.
+- **Beschreibung:** `handleOllamaChat` rief `OllamaApiSupport.toOpenAiChatRequest` vor dem `try` auf, das Ablehnungen auf 400 und Fehler auf 500 abbildet. Der Übersetzer kopiert jedes Element von `messages` mit `ObjectNode message = ollamaMessage.deepCopy()`; für einen String, `null` oder ein Array wirft der vom Compiler eingefügte Cast eine `ClassCastException`. Sie verließ den Handler über `finally { exchange.close(); }` ohne gesendete Kopfzeilen, der JDK-Server schloss die Verbindung ohne Statuszeile und protokollierte nur auf TRACE — gestreamt wie ungestreamt (die Übersetzung liegt vor der Verzweigung). Ein `messages`, das kein Array ist, wurde zudem still als „keine Nachrichten“ gelesen. upstream (`common/chat.cpp`, b11639) lehnt beides mit `std::invalid_argument` ab, also 400.
+- **Fix:** Der Übersetzer prüft die Form und wirft eine `IllegalArgumentException` mit upstreams Wortlaut plus Index (`Expected 'messages[1]' to be an object, got "hi"`, `Expected 'messages' to be an array, got "hi"`); fehlendes und `null` bleiben „keine Nachrichten“. `handleOllamaChat` fasst nur den Übersetzer in ein `try` und beantwortet die `IllegalArgumentException` mit 400 und Ollamas Fehlerkörper, wie `handleResponses` seit F89; eine andere `RuntimeException` bleibt ein protokollierter Serverfehler.
+- **Tests:** `OpenAiCompatServerHttpTest.aNonObjectOllamaChatMessageIsA400NotADroppedConnection` (ungestreamt und gestreamt, das Backend wird nicht gerufen), `OllamaApiSupportTest.aNonObjectMessageIsRejectedWithItsIndex` (String, `null`, Array), `OllamaApiSupportTest.messagesThatAreNoArrayAreRejected`.
+- **Rot-Grün-Nachweis:** Ohne den Fix rot: der HTTP-Test mit `java.net.SocketException: Unexpected end of file from server` aus `getResponseCode()`, `aNonObjectMessageIsRejectedWithItsIndex` mit „Unexpected exception type thrown, expected:  but was: “, `messagesThatAreNoArrayAreRejected` mit „Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“; mit dem Fix grün (`OllamaApiSupportTest` 39, `OpenAiCompatServerHttpTest` 65, `OpenAiRequestMapperTest` 35, `LlamaArchitectureTest` 13), `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `6295831`
+
+
+### F214 – java-llama.cpp: Eine `IOException` des Backends vor dem ersten Chunk gilt als Verbindungsabbruch — keine Antwort, nur DEBUG
+- **Zeit:** 2026-10-20 20:09 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java` (`ResponseStream`; die `IOException`-Zweige von `streamCompletions`, `streamChat`, `streamOllamaChat`, `streamAnthropic`, `streamResponses`)
+- **Schwere:** niedrig (nur über die öffentliche Schnittstelle `OpenAiBackend` erreichbar; `LlamaModelBackend` reicht nur die `IOException` der Senke weiter)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J1-2). Eine Folge von F182: vorher sendete jede Streaming-Route die Kopfzeilen vor dem Backend-Aufruf, der Client sah wenigstens ein 200 mit leerem Stream.
+- **Beschreibung:** `OpenAiBackend.stream`/`streamCompletions` dürfen laut Javadoc eine `IOException` werfen, wenn „generation fails“; der ungestreamte Pfad beantwortet sie mit 500. Die fünf Streaming-Routen fingen sie mit `LOG.debug("client disconnected during stream", e)` und schrieben nichts. Seit F182 beginnt die Antwort erst mit dem ersten Schreiben, also endete eine solche Anfrage mit `exchange.close()` ohne Kopfzeilen — der JDK-Server schloss die Verbindung ohne Statuszeile, entgegen dem CHANGELOG-Eintrag zu F182 („a failure before it is `400` or `500` with the route's own error body“). Dazu: Ein Client, der gegangen ist, fällt oft erst beim Schreiben der Kopfzeilen auf; `answerIfNotStarted` sah dann `os == null` und hätte die Anfrage ein zweites Mal beantworten wollen.
+- **Fix:** Die fünf `IOException`-Zweige beantworten die Anfrage mit 500 und dem Fehlerkörper der Route, solange der Stream nicht begonnen hat (WARN), und bleiben sonst beim DEBUG für den Abbruch. `ResponseStream` merkt sich den Start vor dem Schreiben der Kopfzeilen (`started`), sodass ein dort gescheiterter Start als begonnen gilt: `answerIfNotStarted` gibt `false`, ein weiteres Schreiben wirft, statt die Kopfzeilen erneut zu senden. `answerIfNotStarted` beantwortet eine schon beantwortete Anfrage nicht ein zweites Mal.
+- **Tests:** `OpenAiCompatServerHttpTest.aBackendIOExceptionBeforeTheFirstChunkIsA500OnEveryStreamingRoute` (alle fünf Streaming-Routen, `IoFailingBackend`), `ResponseStreamTest.aStreamWhoseHeadersCouldNotBeWrittenHasStarted`.
+- **Rot-Grün-Nachweis:** Ohne den Fix rot: der HTTP-Test mit `java.net.SocketException: Unexpected end of file from server` aus `getResponseCode()`, der `ResponseStream`-Test mit „Expected: is  but: was “ (`answerIfNotStarted` nach gescheitertem Kopfzeilen-Schreiben); mit dem Fix grün (`ResponseStreamTest` 5, `OpenAiCompatServerHttpTest` 66, `OllamaApiSupportTest` 39, `LlamaArchitectureTest` 13). Modellgestützt mit dem Winzmodell `OpenAiCompatServerIntegrationTest` 13/13 grün; in `OpenAiServerCompletionIntegrationTest` scheitern nur die beiden Infill-Fälle (500, das Winzmodell hat keine FIM-Token), die keine Streaming-Route berühren. `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `d00936b`
+- **Nachtrag (Branch-Review 2026-10-20/21):** die Hälfte nach dem Start des Streams (eine Backend-`IOException` nach dem ersten Chunk galt weiter als Abbruch, ohne Fehler-Event) ist als [F262](#f262) behoben: nur die Ausnahme, die ein Schreiben des Streams selbst warf, ist noch ein DEBUG-Abbruch.
+
+
+### F215 – java-llama.cpp: Ollama `/api/generate` mit `raw: true` verwirft `system`, `template` und `context` still — Ollama antwortet 400
+- **Zeit:** 2026-10-20 20:20 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java` (neu `hasRawConflict`, `RAW_CONFLICT_MESSAGE`; Javadoc von `isRaw`), `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java` (`handleOllamaGenerate`)
+- **Schwere:** niedrig (der Client bekommt eine plausible Antwort, die seinen Systemprompt nie gesehen hat)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Funde J1-3, J3-2, J16-1 — derselbe Fehler). Eingeführt mit F179, dessen Fix-Zeile, der CHANGELOG-Eintrag und der Test `ollamaGenerateWithRawCompletesTheBarePromptAndIgnoresSystem` das stille Verwerfen als Ollamas Verhalten beschrieben bzw. festschrieben.
+- **Beschreibung:** Der `raw`-Zweig von `handleOllamaGenerate` schickte `toOpenAiCompletionRequest` an das Backend, das nur `prompt`, Sampling und `stop` übernimmt; `system`, `template` und `context` gingen ohne Meldung verloren. Ollamas `GenerateHandler` (`server/routes.go`, seit v0.3.0 unverändert) prüft zuerst `if req.Raw && (req.Template != "" || req.System != "" || len(req.Context) > 0)` und antwortet 400 `{"error":"raw mode does not support template, system, or context"}` — vor der Ladeantwort und vor dem Suffix-Weg.
+- **Fix:** `OllamaApiSupport.hasRawConflict` prüft genau Ollamas Bedingung (nicht leeres `system`/`template`, nicht leeres `context`-Array; leere Werte sind kein Konflikt); `handleOllamaGenerate` beantwortet einen Konflikt direkt nach dem Einlesen, vor Ladeantwort und FIM, mit 400 und Ollamas Wortlaut, ohne das Backend zu rufen — gestreamt wie ungestreamt. CHANGELOG-Zeile und die Fix-Zeile von F179 sind korrigiert. Nicht Teil dieses Fixes: `format` und `images` im `raw`-Weg (Ollama behält sie, hier fallen sie weg) und `template`/`context` im Weg ohne `raw` (Ollama wendet sie an) — beides offen.
+- **Tests:** `OpenAiCompatServerHttpTest.ollamaGenerateWithRawAndASystemPromptIsRejectedLikeOllama` (`system`, `template`, `context`, mit `suffix`, ohne Prompt; das Backend wird nicht gerufen) ersetzt `ollamaGenerateWithRawCompletesTheBarePromptAndIgnoresSystem`; `ollamaGenerateWithRawCompletesTheBarePrompt` (der Weg ohne Konflikt); `OllamaApiSupportTest.rawWithSystemTemplateOrContextConflictsLikeOllama` (vier Konflikte, vier zulässige Formen).
+- **Rot-Grün-Nachweis:** Gegen den unveränderten Handler (mit einem `hasRawConflict`, das `false` liefert) rot: der HTTP-Test mit „Expected: is <400> but: was <200>“ für `{"model":"test-model","prompt":"once upon","system":"x","raw":true,"stream":false}`, der Einheitstest mit „Expected: is  but: was “; mit dem Fix grün (`OllamaApiSupportTest` 40, `OpenAiCompatServerHttpTest` 67, `LlamaArchitectureTest` 13), `spotbugs:check` und `javadoc:javadoc` grün.
+- **Nachtrag (Branch-Review 2026-10-21, Abschluss vor 6.0.0):** Der Rest aus der TODO-Liste ist erledigt: `format` und `images` auf dem rohen Weg bildet F259 ab (`format` als `json_schema`, Bilder an ihren `[img-N]`-Marken über den Medien-Marker des Servers; ein Bild ohne Marke ist 400), `template` und `context` ohne `raw` weist F260 mit 400 ab.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `7933eb0`
+
+
+### F216 – java-llama.cpp: Ollama `/api/chat` ohne Nachrichten (Ollamas Vorladen) wird mit 400 abgewiesen statt mit der Ladeantwort beantwortet
+- **Zeit:** 2026-10-20 20:27 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java` (neu `isChatLoadOnly`, `toOllamaChatLoadResponse`), `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java` (`handleOllamaChat`)
+- **Schwere:** niedrig (Clients, die über die Chat-Route vorladen — `ollama-python` `chat(model)` ohne Nachrichten, Open WebUI —, bekommen einen Fehler)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J3-1). Das Gegenstück zu F205, das die Ladeantwort nur für `/api/generate` einführte.
+- **Beschreibung:** Ollama beantwortet ein `/api/chat` ohne Nachrichten mit dem Laden des Modells (`ChatHandler`: `if len(req.Messages) == 0` → `message:{role:"assistant",content:""}`, `done: true`, `done_reason "load"`, ein Objekt ohne Blick auf `stream`; in Go ist `len` eines fehlenden oder `null`-Slices 0). Hier übersetzte `handleOllamaChat` die Anfrage zu `messages: []`, und `OpenAiRequestMapper` wies sie mit „'messages' must be a non-empty array“ ab → 400, auch im Streaming-Weg, den ein Body ohne `stream` nimmt.
+- **Fix:** `isChatLoadOnly` (`messages` fehlt, ist `null` oder ein leeres Array; ein `messages` falschen Typs ist kein Vorladen und wird von der Übersetzung abgewiesen, F213) und `toOllamaChatLoadResponse` (Ollamas Form); `handleOllamaChat` antwortet damit vor der Übersetzung und ohne das Backend, auch bei `stream` ein einzelnes Objekt. `keep_alive: 0` (bei Ollama das Entladen) bekommt wie bei F205 die Ladeantwort: das Modell dieses Servers bleibt geladen. `/v1/chat/completions` weist leere `messages` weiter ab.
+- **Tests:** `OpenAiCompatServerHttpTest.ollamaChatWithoutMessagesOnlyReportsTheModelLoaded` (ohne `messages`, `[]`, `null` mit `stream`, `keep_alive: 0`; das Backend wird nicht gerufen — `RecordingBackend` zeichnet dafür jetzt auch `stream` auf), `OllamaApiSupportTest.aChatRequestWithoutMessagesOnlyLoadsTheModel`.
+- **Rot-Grün-Nachweis:** Gegen den unveränderten Handler (mit Stubs, die `false` bzw. `{}` liefern) rot: der HTTP-Test mit „Expected: is  but: was “ für `done` bei `{"model":"test-model"}` (das Testbackend streamte eine Antwort; das echte Backend weist die leere Konversation mit 400 ab), der Einheitstest mit „Expected: is  but: was “; mit dem Fix grün (`OllamaApiSupportTest` 41, `OpenAiCompatServerHttpTest` 68, `LlamaArchitectureTest` 13), `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `a4d0604`
+- **Nachtrag (Branch-Review 2026-10-20, Fund J1-4):** Die Chat-Route ist von F244 nicht betroffen: `isChatLoadOnly` prüft `messages` auf fehlend, `null` oder leeres Array, und ein `messages` falschen Typs weist die Übersetzung ab (F213). Auf `/api/generate` las `isLoadOnly` dagegen einen `prompt` falschen Typs als leer und gab die Ladeantwort; das behebt [F244](#f244), mit derselben Regel wie hier: nur die Abwesenheit ist Vorladen, ein falscher Typ ist 400.
+
+
+### F217 – java-llama.cpp: Ollamas `think: true` wird verworfen — ein Server mit abgeschaltetem Denken übergeht es still
+- **Zeit:** 2026-10-20 20:31 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java` (`toOpenAiChatRequest`; über `GENERATE_CHAT_FIELDS` auch `/api/generate`)
+- **Schwere:** niedrig (nur bei einem Server, der das Denken per Vorgabe abschaltet; keine Regression — vor F78 wurde `think` ganz ignoriert)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J3-4). F78 übersetzte `think: false` und die Stufen, ließ `true` aber „bei der Vorgabe der Vorlage“; die Fix-Zeile von F78 ist korrigiert.
+- **Beschreibung:** upstream (`server-common.cpp`, b11639) setzt `inputs.enable_thinking` zuerst auf die Vorgabe des Servers (`--reasoning off` schaltet sie ab, ebenso das veraltete `--chat-template-kwargs '{"enable_thinking":false}'`) und lässt dann ein `enable_thinking: true` der Anfrage darüber gewinnen. Ein OpenAI-Client kann das Denken also ausdrücklich einschalten; Ollamas `think: true` („should the model think before responding“) wurde dagegen verworfen und von einem solchen Server still übergangen. (`--reasoning-budget 0` ist ein Sampler-Budget, das `enable_thinking` nicht aufhebt; das bleibt unverändert.)
+- **Fix:** `think: true` wird zu `chat_template_kwargs: {"enable_thinking": true}` als JSON-Boolean (upstream weist einen String ab); `false` und die Stufen bleiben wie mit F78. Auf einer Vorlage ohne Denken ist der Schalter wie bei upstreams OpenAI-Route eine wirkungslose Template-Variable, kein Fehler (Ollama selbst antwortet dort 400) — bewusst wie die OpenAI-Oberfläche, über die die Übersetzung läuft.
+- **Tests:** `OllamaApiSupportTest.thinkTrueSwitchesThinkingOnExplicitly` (Übersetzer → `OpenAiRequestMapper` → `InferenceParameters.toJson()`, für `/api/chat` und `/api/generate`).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot: „Expected: is  but: was “ (`enable_thinking` fehlte in `chat_template_kwargs`); mit dem Fix grün (`OllamaApiSupportTest` 42, `OpenAiRequestMapperTest` 35, `OpenAiCompatServerHttpTest` 68, `LlamaArchitectureTest` 13), `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `eade620`
+
+
+### F218 – java-llama.cpp: Chat-Anfrage mit `json_schema` und `grammar` verliert das Schema still statt upstreams 400
+- **Zeit:** 2026-10-20 21:24 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java` (`applyLlamaCppSampling`)
+- **Schwere:** mittel (stiller Fallback: der Client bekommt 200 und eine Antwort ohne sein Schema)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J2-1). F95 führte die Regel „`json_schema` nur ohne `grammar`, wie upstream“ ein; das stimmte für das Completion-Schema, das der Mapper damals auch für das gestreamte `/v1/completions` bediente (`server-schema.cpp`, der Alias-Handler lässt die Grammatik gewinnen). Seit F186 reicht `/v1/completions` den Körper wörtlich durch, der Mapper bedient nur noch die beiden Chat-Wege, und deren Parser entscheidet anders.
+- **Beschreibung:** Beide Chat-Wege übergeben das gemappte JSON an upstreams `oaicompat_chat_params_parse` (`jllama_engine.cpp`, `parse_oai_chat_params`). Der weist einen Körper mit beiden Feldern ab: `throw std::runtime_error("Cannot use both json_schema and grammar")` (`server-common.cpp:1300-1304`, b11639) → `jllama_invalid_request` → `InvalidRequestException` → 400, so wie llama-server selbst antwortet. Der Mapper ließ das Schema neben einer Grammatik aber fallen, die Ablehnung konnte nie greifen, und `#jsonSchemaReachesTheMappedRequestUnlessAGrammarIsGiven` pinnte das Verwerfen. Nicht betroffen: `response_format` + `grammar` weist upstream nicht ab (`common/chat.cpp` lässt das Schema gewinnen); `response_format` reicht der Mapper ohnehin wörtlich durch.
+- **Fix:** `grammar` und `json_schema` werden unabhängig voneinander übernommen; über das Paar entscheidet upstreams Chat-Parser mit seinem eigenen Text. Keine Ablehnung im Mapper, damit die Regel bei einem llama.cpp-Bump nicht auseinanderläuft. Javadoc von `applyLlamaCppSampling` berichtigt, die Fix-Zeile von F95 ebenso.
+- **Tests:** `OpenAiRequestMapperTest#jsonSchemaAndGrammarAreBothForwardedSoUpstreamCanRefuseThePair` (ersetzt die zweite Hälfte von `#jsonSchemaReachesTheMappedRequestUnlessAGrammarIsGiven`, deren erste Hälfte als `#jsonSchemaReachesTheMappedRequest` bleibt); modellgestützt `ErrorHandlingTest#testJsonSchemaWithGrammarInAChatIsAnInvalidRequest` (ein Chat mit beiden Feldern ist eine `InvalidRequestException` mit upstreams Text „Cannot use both json_schema and grammar“ — der Beleg, dass das Durchreichen beim Client als 400 ankommt).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot: „Expected: is "object" but: was ""“ (`json_schema` fehlte im gemappten Körper); mit dem Fix grün (`OpenAiRequestMapperTest` 36/36). `ErrorHandlingTest#testJsonSchemaWithGrammarInAChatIsAnInvalidRequest` grün mit dem lokalen Winzmodell (CPU); in der Klasse scheitert unabhängig davon `#testHandleInfillMissingInputSuffix`, weil das Winzmodell keine FIM-Token hat. `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `fea0a1d`
+
+
+### F219 – java-llama.cpp: Chat-Mapper normalisiert `max_tokens` unter -1 und `n` unter 1 still auf die Vorgaben
+- **Zeit:** 2026-10-20 21:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java` (`applyCommonFields`)
+- **Schwere:** niedrig
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J2-3). Dieselbe Form wie F102/F103 (der Mapper behandelt einen Wert anders als upstream), nur auf der anderen Seite des Wertebereichs: F103 übernahm die Obergrenze ab `>= 0`, F180 lehnte `n > 1` ab.
+- **Beschreibung:** upstreams Schema (`server-schema.cpp`, b11639) deklariert `n_predict` (Aliase `max_completion_tokens`, `max_tokens`) mit den harten Grenzen -1 … INT32_MAX und `n_cmpl` (Alias `n`) mit 1 … `n_parallel`; eine Verletzung ist `std::invalid_argument` → `InvalidRequestException` → 400. Der Mapper setzte `n_predict` nur ab `>= 0`: `"max_tokens": -2` (ebenso `n_predict`/`max_completion_tokens`) lief auf den gemappten Wegen ohne Obergrenze, das wörtlich durchgereichte `/v1/completions` antwortet 400. `"n"` erreicht upstream von hier nie (es ist kein `RequestField`), und der Mapper lehnte nur `> 1` ab — `"n": 0` und `-1` liefen still mit einer Antwort. Betroffen: beide Chat-Wege, damit auch Anthropic `/v1/messages` und Responses `/v1/responses` (sie kopieren die Obergrenze, wie der Client sie schickt); Ollama normalisiert `num_predict < -1` schon vorher auf -1 (F104).
+- **Fix:** Jede Obergrenze außer -1 wird durchgereicht, upstreams Schema weist -2 und darunter mit seinem eigenen Text ab; ein ausdrückliches -1 bleibt wie ein fehlendes Feld ungesetzt (upstream nimmt dann `--predict` des Servers, `server-context.cpp`). `n` ungleich 1 ist eine ungültige Anfrage (`IllegalArgumentException` → 400, „'n' must be 1 (one answer is returned) but was N“); die Ablehnung bleibt im Mapper, weil `n` upstream von hier nicht erreicht. Der Kommentar über `0` („generate nothing“) ist aus dem Mapper entfernt, weil er `withNPredict` widersprach; die übrigen Stellen zu `0` gehören zu einem eigenen Fund.
+- **Tests:** `OpenAiRequestMapperTest#tokenCapBelowMinusOneReachesUpstreamsRefusal`, `#zeroOrNegativeChoicesAreRefusedLikeUpstream` (`n` 0 mit Meldung, `n` -1), als Schutz `#explicitTokenCapMinusOneLeavesTheServerDefault`; modellgestützt `ErrorHandlingTest#testNPredictMinusTwoInAChatIsAnInvalidRequest` (ein Chat mit `n_predict` -2 ist eine `InvalidRequestException`, die `n_predict` nennt — der Beleg, dass das Durchreichen beim Client als 400 ankommt).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot: „Expected: is <-2> but: was <-1>“ (`n_predict` fehlte) und „Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“ (`n: 0`); mit dem Fix grün (`OpenAiRequestMapperTest` 39/39, `OpenAiCompatServerHttpTest` 68/68, `AnthropicApiSupportTest` 26/26, `ResponsesApiSupportTest` 54/54, `OllamaApiSupportTest` 42/42). `ErrorHandlingTest#testNPredictMinusTwoInAChatIsAnInvalidRequest` grün mit dem lokalen Winzmodell (CPU); `#testHandleInfillMissingInputSuffix` scheitert unabhängig davon am Winzmodell ohne FIM-Token. `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `63f05e1`
+
+
+### F220 – java-llama.cpp: Chat-Mapper verwirft `top_logprobs` ohne `logprobs` still und liest ein ganzzahliges `logprobs` als `n_probs` 20
+- **Zeit:** 2026-10-20 21:40 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java` (`applyLogprobs`, neu; vorher in `toInferenceParameters`)
+- **Schwere:** niedrig
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J2-4). F123 übernahm `logprobs: true` → `n_probs`, ließ upstreams zwei Ablehnungen aber bewusst weg und las `logprobs` mit Jacksons `asBoolean`.
+- **Beschreibung:** upstreams `oaicompat_chat_params_parse` (`server-common.cpp:1456-1473`, b11639) liest `logprobs` nur als Boolean (`json_value` liefert für eine Zahl oder einen String den Standard `false`), setzt bei `true` `n_probs = top_logprobs` (20 ohne), weist sonst ein nicht-null `top_logprobs` mit „top_logprobs requires logprobs to be set to true“ ab (400; die OpenAI-Spezifikation verlangt dasselbe) und kopiert den Rest des Körpers durch — das Schema liest `logprobs` dann als Alias von `n_probs` (`server-schema.cpp:179-180`, nur ohne `n_probs`). Der Mapper dagegen (a) verwarf ein `top_logprobs` ohne `logprobs` still, weil das gemappte JSON es nicht mehr trug, und (b) las mit `asBoolean` jede von 0 verschiedene Zahl und den String `"true"` als die Chat-Form: `"logprobs": 2` ergab `n_probs` 20 statt upstreams 2, `"n_probs": 5` daneben wurde überschrieben, und ein String, den upstreams Schema mit 400 abweist, lief durch.
+- **Fix:** `applyLogprobs` folgt upstream: `logprobs: true` → `n_probs = top_logprobs` (eine Zahl, sonst 20 wie upstreams `json_value`); ein nicht-null `top_logprobs` ohne `logprobs: true` ist eine ungültige Anfrage mit upstreams Text wörtlich (die WEM-Unterdrückung in `spotbugs-exclude.xml` nennt die Methode deshalb); ohne `n_probs` wird eine Zahl als `n_probs` übernommen, jeder andere nicht-null, nicht-boolesche Wert ist eine ungültige Anfrage („'logprobs' must be a boolean or a number“), wie das Schema ihn abweist. `logprobs: false` lässt `n_probs` weiter ungesetzt (upstreams Alias läse 0, die Vorgabe). Weiter offen, nicht Teil dieses Funds: upstreams zweite Ablehnung („logprobs is not supported with tools + stream“) bleibt unerreichbar, weil der Mapper `logprobs` in `n_probs` übersetzt; die Fix-Zeile von F123 ist berichtigt.
+- **Tests:** `OpenAiRequestMapperTest#topLogprobsWithoutLogprobsIsRefusedLikeUpstream` (ohne, mit `false`, mit `null` und neben einer Zahl), `#anIntegerLogprobsMapsToNProbsLikeUpstreamsSchemaAlias`, `#nProbsWinsOverAnIntegerLogprobsLikeUpstream`, `#aLogprobsThatIsNeitherBooleanNorNumberIsRefused` (mit `n_probs` daneben keine Ablehnung, wie upstream); als Schutz `#topLogprobsNullWithoutLogprobsIsTheDefault`, die drei F123-Tests unverändert grün.
+- **Rot-Grün-Nachweis:** Ohne den Fix vier rot: „Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“ (`top_logprobs: 3` allein bzw. `logprobs: "true"`), „Expected: is <2> but: was <20>“ (`logprobs: 2`), „Expected: is <5> but: was <20>“ (`n_probs: 5` neben `logprobs: 2`); mit dem Fix grün (`OpenAiRequestMapperTest` 44/44, `OpenAiCompatServerHttpTest` 68/68, `AnthropicApiSupportTest` 26/26, `ResponsesApiSupportTest` 54/54, `OllamaApiSupportTest` 42/42, `LlamaArchitectureTest` 13/13). `spotless:apply` grün; `spotbugs:check` meldete zuerst zweimal `WEM_WEAK_EXCEPTION_MESSAGING` für die festen Texte, mit der Unterdrückung grün.
+- **Nachtrag (Branch-Review 2026-10-21, Abschluss vor 6.0.0):** Upstreams dritte `logprobs`-Regel („logprobs is not supported with tools + stream“) ist mit F255 übernommen; der Mapper liest `stream` dafür so, wie der Server den Streaming-Weg wählt.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `ca1baa9`
+
+
+### F221 – java-llama.cpp: Anthropic meldet ein getroffenes Stoppwort als `stop_reason` `end_turn` statt `stop_sequence`
+- **Zeit:** 2026-10-20 22:30 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java` (`toAnthropicResponse`, `anthropicStopReason(String, String)` neu), `AnthropicStreamTranslator.java` (`end`)
+- **Schwere:** mittel (ein Client, der auf `stop_reason` verzweigt — die Anthropic-SDKs typisieren es als Aufzählung, Agentenschleifen setzen bei `end_turn` fort —, konnte einen Stopp durch das Wort nicht von einem natürlichen Ende unterscheiden)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J4-1). Folge von F183, das das Wort über `__verbose.stopping_word` erst verfügbar macht und `stop_reason` dabei bewusst wie upstream auf `end_turn` ließ. upstream (`server-task.cpp:723-726`/`:792-795`, b11639 und master) bildet `STOP_TYPE_WORD` wie `STOP_TYPE_EOS` auf `end_turn`/`tool_use` ab und testet den Wert der Spezifikation nirgends.
+- **Beschreibung:** Die Messages-API legt fest: „If the model encounters one of the custom sequences, the response stop_reason value will be "stop_sequence" and the response stop_sequence value will contain the matched stop sequence.“ `toAnthropicResponse` und `AnthropicStreamTranslator.end()` bildeten den OpenAI-`finish_reason` unabhängig vom Wort ab, `anthropicStopReason` kannte kein `stop_sequence`: Eine Anfrage mit `stop_sequences: ["X"]`, deren Generierung an `X` endete, bekam das widersprüchliche Paar `stop_reason: "end_turn"`, `stop_sequence: "X"` — blockierend wie gestreamt.
+- **Fix:** Neue Überladung `anthropicStopReason(finishReason, stopSequence)`: `stop` mit einem Wort ist `stop_sequence`, alles andere die bisherige Abbildung (`length` → `max_tokens`, `tool_calls` → `tool_use`, sonst `end_turn`). `toAnthropicResponse` liest das Wort jetzt vor dem Stoppgrund, der Stream-Übersetzer übergibt sein gesammeltes Wort. Das Wort ist genau bei einem Wortstopp nicht leer (upstream setzt `stopping_word` nur dort, `server-context.cpp:630`), und ein solcher Stopp hat immer `finish_reason` `stop`, also bleiben `tool_use` und `max_tokens` unberührt. Die Javadoc der Überladung zitiert die Spezifikation und nennt die bewusste Abweichung von upstream. Nicht Teil dieses Commits: `CHANGELOG.md` (dort steht noch „stop_reason stays end_turn“, wird in einem späteren Durchgang nachgezogen); upstream-tauglich als Einzeiler in beiden `to_json_anthropic*`.
+- **Tests:** `AnthropicApiSupportTest#aMatchedStopWordIsReportedAsStopReasonStopSequence`, `#theMatchedStopWordIsTheStopSequence` (Erwartung von `end_turn` auf `stop_sequence` umgestellt), `#onlyAStopThatAStopWordEndedIsStopSequence` (die drei unveränderten Zweige mit Wort und `stop` ohne Wort), als Schutz `#withoutAStopWordAStopIsStillEndTurn`; `AnthropicStreamTranslatorTest#aMatchedStopWordIsTheStopReasonStopSequenceOfTheMessageDelta`, `#theMatchedStopWordIsTheStopSequenceOfTheMessageDelta` (umgestellt).
+- **Rot-Grün-Nachweis:** Ohne den Fix vier rot: „Expected: is "stop_sequence" but: was "end_turn"“ (beide blockierenden Fälle) und „Expected: a string containing "\"stop_reason\":\"stop_sequence\""“ (beide gestreamten); mit dem Fix grün (`AnthropicApiSupportTest` 29/29, `AnthropicStreamTranslatorTest` 8/8, `OpenAiCompatServerHttpTest` 68/68). `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `7e965d9`
+
+
+### F222 – java-llama.cpp: Gestreamte Anthropic-Antwort öffnet bei Denkinhalt nach Text einen zweiten Thinking-Block, während der Textblock offen bleibt
+- **Zeit:** 2026-10-20 22:35 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicStreamTranslator.java` (`onChunk`, `end`, `closeTextBlock` neu)
+- **Schwere:** niedrig (braucht einen Parser, der den Denkinhalt nach begonnenem Text verlängert; die SDK-Akkumulatoren sammeln nach Index und setzen den Inhalt trotzdem zusammen, ein sequenziell darstellender Client hängt Text an einen Block, der sichtbar hinter einem späteren Thinking-Block liegt)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J4-3). F81 führte die Thinking-Blöcke im Stream ein, mit dem erklärten Ziel, dass die Blöcke einander folgen, statt wie bei upstream bis zum Ende offen zu bleiben; der Textpfad schloss dafür einen offenen Thinking-Block, der Reasoning-Pfad den Textblock nicht.
+- **Beschreibung:** `common_chat_msg_diff::compute_diffs` (`common/chat.cpp:236-252`, b11639) sendet ein `reasoning_content`-Delta, wann immer der geparste Denkinhalt wächst, unabhängig davon, ob schon Text kam, und die PEG-Mapper hängen jeden Reasoning-Knoten an (`chat-peg-parser.cpp:321`, „GPT OSS can have more than 1 reasoning block“; Gemma 4: `message = thought + content`, mehrfach). Für Denken „a“, Text „b“, Denken „c“, Text „d“ lieferte der Übersetzer: Thinking 0 (Start, Delta, Signatur, Stopp), Text 1 (Start, Delta „b“), Thinking 2 (Start, Delta „c“) bei noch offenem Text 1, Signatur und Stopp von 2, dann das Delta „d“ wieder an Index 1 und erst in `end()` den Stopp von 1 — zwei Blöcke gleichzeitig offen, ein Delta an einen Block nach dem Stopp des späteren. Die Streaming-Doku beschreibt „a series of content blocks, each of which has a content_block_start, one or more content_block_delta events, and a content_block_stop event“.
+- **Fix:** `closeTextBlock` (Gegenstück zu `closeThinkingBlock`) schließt einen offenen Textblock, bevor ein Thinking-Block geöffnet wird; `end()` nutzt dieselbe Methode. Höchstens ein Block ist offen, das Beispiel ergibt 0 Thinking, 1 Text, 2 Thinking, 3 Text, wie Claude selbst mehrere Thinking-Blöcke streamt. Die Klassen-Javadoc nennt die Regel und die bewusste Asymmetrie: `toAnthropicResponse` kann die Verschränkung nicht abbilden, weil die Chat-Nachricht den Denkinhalt als einen verketteten String trägt.
+- **Tests:** `AnthropicStreamTranslatorTest#reasoningAfterTextClosesTheTextBlockBeforeANewThinkingBlock` (zweiter Thinking-Start an Index 2, letztes Textdelta an Index 3, Stopp von 1 vor dem zweiten Thinking-Start, kein Delta an 1 nach dessen Stopp, `end()` schließt genau einen Block, Index 3); die übrigen Tests der Klasse unverändert grün.
+- **Rot-Grün-Nachweis:** Ohne den Fix rot: „Expected: a string containing "\"index\":3,\"delta\":{\"type\":\"text_delta\",\"text\":\"d\"}"“ (das Delta „d“ ging an Index 1); mit dem Fix grün (`AnthropicStreamTranslatorTest` 9/9, `AnthropicApiSupportTest` 29/29, `OpenAiCompatServerHttpTest` 68/68). `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `cb123a9`
+
+
+### F223 – java-llama.cpp: Anthropic `max_tokens`, das keine Zahl ist, wird still durch die Vorgabe 4096 ersetzt statt abgewiesen
+- **Zeit:** 2026-10-20 22:40 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java` (`toOpenAiChatRequest`), `OpenAiCompatServer.java` (`handleAnthropicMessages`)
+- **Schwere:** niedrig (ein Client mit einem falsch typisierten `max_tokens`, etwa `"100"`, bekam ohne Hinweis eine Obergrenze von 4096 statt seiner)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J4-4). F96 führte die Vorgabe 4096 für ein fehlendes `max_tokens` ein und prüfte dafür „keine Zahl“ statt „fehlt“; vor F96 ließ `copyNumber` einen Nicht-Zahl-Wert ebenso still weg (dann unbegrenzt).
+- **Beschreibung:** Die Messages-API typisiert `max_tokens` als Pflicht-Ganzzahl; upstream (`convert_anthropic_to_oai`, `server-chat.cpp:586-589`, b11639) kopiert den Wert wie er ist, und das Schema (`field_num::eval`, `server-schema.cpp:531-549`) weist einen String oder Boolean mit „Field 'max_tokens': … type must be number“ ab (400), behandelt `null` aber wie ein fehlendes Feld (`has_value`, „clients can send null to request the server default“). Die Übersetzung verzweigte auf `isNumber()`: `"max_tokens": "100"` und `true` liefen still mit 4096. Den Wert einfach durchzureichen hilft hier nicht, weil der Mapper (`OpenAiRequestMapper.readMaxTokens`) eine Nicht-Zahl selbst als -1 (unbegrenzt) liest, bevor upstreams Schema sie sieht. Außerdem rief `handleAnthropicMessages` die Übersetzung außerhalb des `try` auf, das eine `IllegalArgumentException` als 400 beantwortet: Eine Ablehnung wäre als abgebrochene Verbindung angekommen.
+- **Fix:** Eine Zahl wird kopiert, ein fehlendes Feld oder `null` bekommt wie bisher 4096, jeder andere Wert ist eine `IllegalArgumentException` („'max_tokens' must be an integer but was …“). `handleAnthropicMessages` übersetzt in einem eigenen `try` vor der Verzweigung auf `stream` (wie `handleResponses`) und antwortet mit Anthropics 400 `invalid_request_error`, gestreamt wie blockierend, bevor ein SSE-Strom beginnt. Nicht Teil dieses Funds: Dieselbe stille Klasse besteht auf der OpenAI-Route selbst (`OpenAiRequestMapper.readMaxTokens` liest `"100"` als -1 = unbegrenzt, upstream antwortet 400) — eigener Fund.
+- **Tests:** `AnthropicApiSupportTest#aMaxTokensThatIsNotANumberIsRefused` (`"100"` und `true`, Meldung nennt `max_tokens`), als Schutz `#requestWithNullMaxTokensIsCappedLikeAbsent` (vorher und nachher grün) und `#requestWithoutMaxTokensIsCappedLikeUpstream`; `OpenAiCompatServerHttpTest#aStringMaxTokensOnTheAnthropicRouteIsA400InvalidRequestError` (`stream` false und true: 400, `"type":"invalid_request_error"`, nichts generiert).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot: „"100" ==> Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“ und „Expected: is <400> but: was <200>“; nur mit der Übersetzungsseite des Fixes scheiterte der HTTP-Test mit „java.net.SocketException: Unexpected end of file from server“ (der Beleg, dass die Ablehnung ohne das verlegte `try` als abgebrochene Verbindung ankäme); mit dem ganzen Fix grün (`AnthropicApiSupportTest` 31/31, `AnthropicStreamTranslatorTest` 9/9, `OpenAiCompatServerHttpTest` 69/69). `spotless:apply` und `spotbugs:check` grün.
+- **Nachtrag (Branch-Review 2026-10-21, Abschluss vor 6.0.0):** Der hier ausgenommene OpenAI-Weg ist mit F253 geschlossen: der Mapper weist jedes Zahlenfeld mit falschem Typ ab, und die Anthropic-Übersetzung liest `max_tokens` (dazu `temperature`, `top_p`, `top_k`) über denselben Leser `OpenAiRequestMapper.optionalNumber`; die Meldung lautet jetzt wie upstreams Schema „Field 'max_tokens': type must be number, but is string“ (`AnthropicApiSupportTest#aMaxTokensThatIsNotANumberIsRefused` prüft weiter `containsString("max_tokens")`, der HTTP-Test bleibt grün).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `82bef4a`
+
+
+### F224 – java-llama.cpp: Responses `input_image` ohne URL und fremde Werkzeugergebnis-Teile werden still verworfen statt abgewiesen
+- **Zeit:** 2026-10-20 23:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java` (`inputContentWithImages`, `toolOutputParts`, neu `imageUrl`)
+- **Schwere:** niedrig (ein Bild per `file_id` oder ein Werkzeugergebnis mit einem `output_text`-Teil kam ohne Hinweis nicht beim Modell an; es antwortete, als wäre es nicht geschickt worden)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J5-1). F17 ließ `file_id`-Bilder bewusst weg („werden weiterhin ignoriert“), F86 übersprang fremde Teiltypen eines Werkzeugergebnisses („upstream weist sie ab“); F89 hatte dieselbe Klasse für `input_file` schon in eine 400 verwandelt.
+- **Beschreibung:** `inputContentWithImages` nahm einen `input_image`-Teil nur mit einer nicht leeren URL auf; `{"type":"input_image","file_id":"file-1"}` (nach dem OpenAI-Typ `ResponseInputImageParam` gültig, `image_url` und `file_id` sind beide optional) fiel weg, die Nachricht ging als reiner Text ans Modell. `toolOutputParts` übernahm nur `input_text` und ein `input_image` mit einem String-`image_url`; jeder andere Teil, ein Teil ohne `type` und die Objektform `{"url": …}` (die der Nachrichtenzweig annimmt) fielen weg, ein `[{"type":"output_text",…}]` ergab eine Werkzeugnachricht mit leerem Inhalt. upstream (`convert_responses_to_chatcmpl`, `server-chat.cpp:83-85, 207-216`, b11639) antwortet in jedem dieser Fälle 400: „'image_url' is required“, „Output of tool call missing 'type' field“, „Output of tool call should be 'Input text' or 'Input image'“.
+- **Fix:** Ein gemeinsamer Helfer `imageUrl` liest die URL auf beiden Wegen gleich (String oder `{url}`, eine bewusst beibehaltene Erweiterung von F17). Ein `input_image` ohne URL ist eine `IllegalArgumentException` mit upstreams Text plus der `file_id`; im Werkzeugergebnis ebenso ein Teil ohne `type` und ein Teil eines anderen Typs, jeweils mit upstreams Text, dem Typ und der `call_id`. `handleResponses` beantwortet die Ausnahme schon als 400 (F89). Bewusst schärfer als upstream: ein leerer `image_url`-String gilt als fehlend (upstream scheitert daran später in `handle_media`). Nicht geändert: unbekannte Teiltypen einer `message` bleiben Text bzw. werden übergangen, weil derselbe Zweig Assistenten-Nachrichten mit `output_text`/`refusal` trägt (F173).
+- **Tests:** `ResponsesApiSupportTest#inputImageWithoutAUrlIsRejectedLikeUpstream`, `#toolOutputPartOfAnotherTypeIsRejectedLikeUpstream`, `#toolOutputPartWithoutATypeIsRejectedLikeUpstream`, `#toolOutputImageWithoutAUrlIsRejectedLikeUpstream`, `#toolOutputImageInObjectFormKeepsItsUrl`; die 400-Abbildung pinnt schon `OpenAiCompatServerHttpTest#responsesWithAnInputFileIsRejectedLikeUpstream`.
+- **Rot-Grün-Nachweis:** Ohne den Fix rot (5 Fehlschläge: viermal „Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“, einmal „Expected: is "image_url" but: was ""“), mit dem Fix grün (`ResponsesApiSupportTest` 59/59, `ResponsesStreamTranslatorTest` 5/5, `OpenAiCompatServerHttpTest` 69/69). `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `e372019`
+
+
+### F225 – java-llama.cpp: Responses `function_call` mit Objekt-`arguments` wird still zu `"{}"`
+- **Zeit:** 2026-10-20 23:07 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java` (`appendInput`, Fall `function_call`)
+- **Schwere:** niedrig (ein Client, der die Argumente eines früheren Aufrufs als Objekt statt als JSON-Text zurückschickt, bekam einen Verlauf, in dem der Aufruf ohne Argumente gemacht wurde)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J5-2). F172 machte fehlende, leere und nur aus Leerraum bestehende `arguments` zu `"{}"` (Entscheidung des Nutzers, wie F166); vor F172 ergab ein Objekt `""`, das upstreams `json::parse` mit 400 abwies.
+- **Beschreibung:** Die Übersetzung las `item.path("arguments").asText("")`. Für ein Objekt, ein Array, eine Zahl oder einen Wahrheitswert liefert Jacksons `asText` bei Containern `""` (Zahlen und Booleans als ihren Text), also wurde `"arguments": {"city":"Paris"}` zu `"{}"` – das Modell sah einen Aufruf, den es so nie gemacht hatte, ohne jeden Hinweis. Der OpenAI-Typ (`ResponseFunctionToolCallParam`) verlangt einen JSON-String, und upstream (`convert_responses_to_chatcmpl`, `server-chat.cpp:163-168, 256-257`, b11639) nimmt ein `function_call`-Item nur mit String-`arguments` an und antwortet sonst 400 („Cannot determine type of 'item'“). F172s Entscheidung deckte nur fehlende/leere Argumente, nicht diesen Fall.
+- **Fix:** Ein `arguments`-Wert, der weder fehlt noch `null` noch ein String ist, ist eine `IllegalArgumentException` („'arguments' of a function_call must be a JSON string (got OBJECT, call_id c1)“), die `handleResponses` als 400 beantwortet. Fehlend, `null` und leer bleiben nach F172 `"{}"`, ein String geht unverändert weiter. Die verlustfreie Alternative (das Objekt als JSON-Text weiterreichen) wurde nicht gewählt: sie ginge über den OpenAI-Typ und upstream hinaus.
+- **Tests:** `ResponsesApiSupportTest#functionCallWithArgumentsThatAreNotAStringIsRejectedLikeUpstream` (Objekt, Array, Zahl, Boolean; Meldung nennt `'arguments'` und die `call_id`); `#functionCallWithoutArgumentsBecomesAnEmptyArgumentsObject` um `"arguments":null` erweitert (vorher und nachher grün, pinnt, dass `null` weiter `"{}"` ist).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot („{"city":"Paris"} ==> Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“), mit dem Fix grün (`ResponsesApiSupportTest` 60/60, `OpenAiCompatServerHttpTest` 69/69). `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `4913acf`
+
+
+### F226 – java-llama.cpp: Responses-Route verwirft `logit_bias` und `n_probs` still, obwohl der Chat-Mapper sie liest
+- **Zeit:** 2026-10-20 23:12 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java` (`CHAT_PASSTHROUGH_FIELDS` und seine Javadoc)
+- **Schwere:** niedrig (ein per `logit_bias` gesperrtes Token wurde auf `/v1/responses` trotzdem erzeugt, ohne Hinweis)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J5-3). Die Liste entstand mit F106 (`b370643`) gegen den damaligen Mapper; F180 (`cceaaa0`) brachte dem Mapper `logit_bias` und `n_probs` bei und vermerkte, das erreiche auch die Responses-Übersetzung – die Liste filtert aber, was kopiert wird, und wurde nicht nachgezogen. F183/F202 fügten `verbose` und `response_fields` hinzu, ebenfalls ohne die Liste.
+- **Beschreibung:** Upstreams `convert_responses_to_chatcmpl` (`server-chat.cpp:15`, b11639) beginnt mit `json chatcmpl_body = response_body;` und kopiert damit jedes Feld; ein `logit_bias` auf upstreams `/v1/responses` erreicht das Schema (`server-schema.cpp:382`) und wirkt. Hier kopierte die Übersetzung nur die Felder ihrer Liste, deren Javadoc behauptete, es seien die, die der Chat-Mapper liest; `logit_bias` und `n_probs` fehlten, die Anfrage lief ohne sie. Der parametrisierte Test zählte nur die Felder der Liste auf und konnte eine Lücke nicht sehen. Auch die CHANGELOG-Zeile zu F180 (alle Chat-Routen einschließlich Responses reichen `logit_bias`/`n_probs` weiter) stimmte damit nicht.
+- **Fix:** `logit_bias` und `n_probs` stehen in der Liste (`n_probs` aus Parität mit upstreams Annahme, sichtbar wird es nicht: die Responses-Ausgabe trägt hier wie upstream keine Wahrscheinlichkeiten). Die Javadoc sagt jetzt, was die Liste ist (die vom Mapper gelesenen Felder, deren Wirkung einen Responses-Client erreichen kann) und nennt die bewussten Auslassungen: die oben übersetzten Strukturfelder, `verbose`/`response_fields` (die Responses-Ausgabe trägt weder das Debug-Objekt noch eine Projektion; weitergereicht zöge `verbose` nur upstreams Debug-Objekt durch JNI, um es zu verwerfen, die Kosten, die F202 auf der Anthropic-Route entfernte) und chats `logprobs`/`top_logprobs` (die Responses-Form ist `top_logprobs` mit `include`, die Ausgabe trägt keine Wahrscheinlichkeiten). Der vorgeschlagene reflektive Wächter über die `request.path(…)`-Literale des Mappers wurde nicht gebaut (er müsste Quelltext lesen und eine große Ausnahmeliste pflegen); die Ausnahmeliste der Javadoc ist der Vertrag.
+- **Tests:** `ResponsesApiSupportTest#requestFieldsTheChatMapperReadsPassThroughLikeUpstream` um die Zeilen `logit_bias|{"42":-100}` und `n_probs|3` erweitert.
+- **Rot-Grün-Nachweis:** Ohne den Fix rot („Expected: is <{"42":-100}> but: was <>“ und „Expected: is <3> but: was <>“), mit dem Fix grün (`ResponsesApiSupportTest` 62/62, `OpenAiCompatServerHttpTest` 69/69, `OpenAiRequestMapperTest` 44/44). `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `88f823e`
+
+
+### F227 – java-llama.cpp: `tool_choice`, das eine Funktion benennt, lässt bei mehreren Werkzeugen jedes zu
+- **Zeit:** 2026-10-20 23:21 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java` (`toOpenAiChatRequest`, Werkzeugblock), `OpenAiRequestMapper.java` (`toInferenceParameters`, neu `namedToolChoice`)
+- **Schwere:** niedrig (mit mehreren Werkzeugen durfte das Modell statt des verlangten ein anderes aufrufen)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J5-4). F109 bildete `{"type":"function","name":…}` (Responses) und `{"type":"function","function":{"name":…}}` (Chat) auf `required` ab und vermerkte, das sei nur „mit einem einzigen Werkzeug genau das verlangte“.
+- **Beschreibung:** llama.cpp kennt für `tool_choice` nur `auto`/`none`/`required` (`common_chat_tool_choice_parse_oaicompat`, `common/chat.cpp:314-325`, b11639) und baut die Werkzeug-Grammatik über alle übergebenen Werkzeuge. `required` über die ganze Liste ließ deshalb jeden Aufruf zu; die OpenAI-Semantik („force the model to call a specific function“, `ToolChoiceFunctionParam`) galt nur bei genau einem Werkzeug. Ein unbekannter Name und ein `{"type":"function"}` ohne Namen liefen still als `required` über alle Werkzeuge; OpenAI weist einen unbekannten Namen mit 400 ab. upstream selbst liest das Objekt gar nicht (`json_value(body, "tool_choice", "auto")` fällt auf `auto` zurück), die Übersetzung war upstream also schon voraus.
+- **Fix:** Ein benanntes `tool_choice` wird durch Einengen der weitergereichten Werkzeugliste erzwungen: nur das genannte Funktionswerkzeug geht weiter, dazu `required`; so lässt die Grammatik keinen anderen Aufruf zu. Ein Name, den kein Funktionswerkzeug trägt, ist eine `IllegalArgumentException` („tool_choice names unknown tool 'g'“), ein Objekt vom Typ `function` ohne Namen ebenso (beides 400). Beide Wege nutzen denselben Helfer `OpenAiRequestMapper.namedToolChoice`; die Responses-Übersetzung filtert schon beim Kopieren der Werkzeuge. Folge, in der Javadoc genannt: die Chat-Vorlage zeigt dem Modell die anderen Werkzeugdefinitionen nicht mehr (OpenAI lässt sie sichtbar und beschränkt nur die Ausgabe) – mit llama.cpps dreiwertigem `tool_choice` der Preis der Genauigkeit. Die Klassen-Javadoc des Mappers nennt `tool_choice` nur noch in seiner String-Form als unverändert weitergereicht. Nicht Teil dieses Funds: Anthropics `{"type":"tool","name":…}` wird weiter zu `required` über alle Werkzeuge, und ein `tool_choice`-Objekt anderen Typs (`allowed_tools`, `mcp`, …) fällt auf beiden Wegen weiter still auf `auto`.
+- **Tests:** `ResponsesApiSupportTest#toolChoiceNamingAFunctionForwardsOnlyThatTool`, `#toolChoiceNamingAnUnknownFunctionIsRejected`, `#toolChoiceOfTypeFunctionWithoutANameIsRejected` und dieselben drei in `OpenAiRequestMapperTest`; die F109-Tests `#toolChoiceNamingAFunctionRequiresAToolCall` (ein Werkzeug) beider Klassen bleiben grün.
+- **Rot-Grün-Nachweis:** Ohne den Fix rot (6 Fehlschläge: je Klasse „Expected: is <1> but: was <2>“ und zweimal „Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“), mit dem Fix grün (`ResponsesApiSupportTest` 65/65, `OpenAiRequestMapperTest` 47/47, `OpenAiCompatServerHttpTest` 69/69, `AnthropicApiSupportTest` 31/31, `OllamaApiSupportTest` 42/42, `LlamaArchitectureTest` 13/13). `spotless:apply` und `spotbugs:check` grün (eine erste Fassung mit fester Meldung ohne Namen fiel bei SpotBugs als `WEM_WEAK_EXCEPTION_MESSAGING` durch; die Meldung nennt jetzt das `tool_choice`).
+- **Nachtrag (Branch-Review 2026-10-21, Abschluss vor 6.0.0):** Die beiden hier ausgenommenen Reste sind mit F254 geschlossen: Anthropics `{"type":"tool","name":…}` wird OpenAIs benanntes Objekt und vom Mapper genauso eingeengt, und ein `tool_choice`-Objekt anderen Typs (`allowed_tools`, `mcp`, …) ist auf allen drei Wegen eine 400 statt eines stillen `auto`.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `740938d`
+
+
+### F228 – java-llama.cpp: Responses-Eingabe-Items unbekannten Typs werden zu leeren Nutzernachrichten statt abgewiesen
+- **Zeit:** 2026-10-20 23:26 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesApiSupport.java` (`appendInput`, `switch` über den Item-Typ; Klassen-Javadoc)
+- **Schwere:** niedrig (ein Verlauf mit z. B. Codex' `custom_tool_call`/`custom_tool_call_output` oder einem `item_reference` erreichte das Modell mit leeren Nutzerzügen an Stelle der Aufrufe, ohne Hinweis)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J5-5). Der gemeinsame `case "message": default:`-Zweig stammt aus der Zeit vor dem Branch; der Branch schrieb den `switch` um (Reasoning, Zusammenführung, Werkzeugteile, `input_file`, F17/F86/F89/F99) und kündigt im CHANGELOG an, was nicht bedient werden kann, werde mit 400 abgewiesen.
+- **Beschreibung:** `appendInput` las `item.path("type").asText("message")` und führte jeden unbekannten Typ durch den Nachrichtenzweig. Ohne `role` und meist ohne `content` wurde daraus `{"role":"user","content":""}`; die OpenAI-Union `ResponseInputItemParam` kennt über 30 Item-Typen (`custom_tool_call`, `custom_tool_call_output`, `item_reference`, `mcp_call`, `apply_patch_call`, …). upstream (`convert_responses_to_chatcmpl`, `server-chat.cpp:256-257`, b11639) beendet jedes nicht erkannte Item mit „Cannot determine type of 'item'“ (400).
+- **Fix:** Der `message`-Fall endet mit `break`; `default` wirft eine `IllegalArgumentException` mit upstreams Text und dem Typ („Cannot determine type of 'item' (custom_tool_call)“), die `handleResponses` als 400 beantwortet. Ein Item ohne `type` (die EasyInputMessage-Form, deren `type` optional ist) landet wie bisher über den Vorgabewert `"message"` im Nachrichtenzweig. Die Klassen-Javadoc nennt die Item-Typen und die Abweisung. Bewusst lockerer als upstream geblieben: eine Nachricht mit einer Rolle außerhalb von user/system/developer/assistant und eine Assistenten-Nachricht ohne `type` werden weiter angenommen (vom Fund nicht verlangt).
+- **Tests:** `ResponsesApiSupportTest#inputItemOfAnUnknownTypeIsRejectedLikeUpstream` (Meldung nennt upstreams Text und `custom_tool_call`), als Schutz `#inputItemWithoutATypeStillMapsToAMessage` (vorher und nachher grün).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot („Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“), mit dem Fix grün (`ResponsesApiSupportTest` 67/67, `ResponsesStreamTranslatorTest` 5/5, `OpenAiCompatServerHttpTest` 69/69). `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `beaffcc`
+
+
+### F229 – java-llama.cpp: Abgeschnittene Responses-Antwort (`finish_reason` `length`) wird als `completed` gemeldet
+- **Zeit:** 2026-10-20 23:33 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/ResponsesStreamTranslator.java` (`onChunk`, `end`), `ResponsesApiSupport.java` (`toResponsesResponse`, neu `isTruncated`, `markIncomplete`)
+- **Schwere:** niedrig (ein Client, dessen Antwort an `max_output_tokens` abgeschnitten wurde, sah eine vollständige Antwort)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J5-7). Seit es die Responses-Route gibt; upstreams `to_json_oaicompat_resp` (`server-task.cpp:579`, `:688`, b11639) meldet ebenfalls immer `completed`, es ist also Parität mit upstream, keine Regression. Bewusst über upstream hinaus wie F107 (Anthropics `tool_choice` `none`); vLLM verfährt so (PR #24561).
+- **Beschreibung:** `ResponsesStreamTranslator.end()` und `ResponsesApiSupport.toResponsesResponse` bauten die Antwort fest mit `status` `completed`, der Strom endete immer mit `response.completed`; `finish_reason` wurde nie gelesen. Die Übersetzung bildet `max_output_tokens` auf `max_tokens` ab, upstreams Chunks tragen bei `STOP_TYPE_LIMIT` `finish_reason` `"length"`. Die Responses-API definiert dafür `status` `incomplete`, `incomplete_details.reason` `max_output_tokens`, das Ereignis `response.incomplete` und den Nachrichtenstatus `incomplete` (openai-python `Response`, `ResponseIncompleteEvent`, `ResponseOutputMessage`).
+- **Fix:** Der Translator merkt sich das letzte textuelle `finish_reason` (der angehängte reine Usage-Chunk mit `"choices":[]` überschreibt es nicht, `null` der Zwischenchunks ebenso wenig). Bei `"length"` sind die Antwort `incomplete` mit `incomplete_details: {"reason":"max_output_tokens"}`, das Nachrichten-Item (in `response.output_item.done` und im `output`) `incomplete` und das letzte Ereignis `response.incomplete`; Reasoning- und `function_call`-Items bleiben `completed`, `completed_at` wird nicht gesetzt (die Hülle trug es nie). Dasselbe blockierend aus `choices[0].finish_reason`. Die Javadoc von `isTruncated` sagt, dass llama.cpps `"length"` auch einen vollen Kontext ohne Kontextverschiebung, `n_indent` und `t_max_predict_ms` umfasst; `max_output_tokens` ist der nächste der definierten Gründe, die Meldung also grob, nicht still.
+- **Tests:** `ResponsesStreamTranslatorTest#maxTokensCutOffEndsWithResponseIncomplete` (Text, `length`, dann der reine Usage-Chunk; `response.incomplete`, kein `response.completed`, Status und Grund, Nachrichten-Item `incomplete`), `#aStoppedAnswerStaysCompleted`; `ResponsesApiSupportTest#responseCutOffAtTheTokenLimitIsIncomplete`, `#responseWrapsOutputMessageWithOutputTextAndUsage` prüft zusätzlich, dass eine `stop`-Antwort kein `incomplete_details` trägt. Die modellgestützten `OpenAiCompatServerIntegrationTest#responsesNonStreamingRoundTrip`/`#responsesStreamingRoundTrip` erwarteten mit `max_output_tokens` 16 `completed`; sie fragen jetzt eine Antwort ab, die in 4 Token nicht passt („Count from 1 to 100 …“), und erwarten `incomplete`/`response.incomplete` mit dem Grund – deterministisch statt davon abhängig, ob das Modell vor 16 Token aufhört.
+- **Rot-Grün-Nachweis:** Ohne den Fix rot („Expected: is "incomplete" but: was "completed"“ und „Expected: a string containing "event: response.incomplete" but: was "event: response.output_text.done …"“); umgekehrt scheiterten nach dem Fix die unveränderten Integrationstests mit dem lokalen Winzmodell („Expected: is "completed" but: was "incomplete"“, der Beweis, dass der Fix den echten Server erreicht); mit dem Fix und den angepassten Integrationstests grün (`ResponsesApiSupportTest` 68/68, `ResponsesStreamTranslatorTest` 7/7, `OpenAiCompatServerHttpTest` 69/69, `OpenAiCompatServerIntegrationTest` 13/13 mit dem Winzmodell, zweimal). `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `ad8b0b1`
+
+
+### F230 – java-llama.cpp: `/v1/rerank`: ein `top_n` als Gleitkommazahl gilt als „fehlt“, ein negatives (`-1.5`) umgeht die Abweisung
+- **Zeit:** 2026-10-21 00:14 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OaiRerankSupport.java` (`readTopN`)
+- **Schwere:** niedrig (still abweichend von upstream; die Abweisung aus F177 griff für die Gleitkomma-Schreibweise nicht)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J6-1). Die Hälfte „Gleitkommazahl = fehlt“ ist älter als der Branch (`return topN.isInt() ? topN.asInt() : -1;`); F177 setzte die Prüfung auf negative Werte hinter dieselbe `isInt()`-Bedingung, daher erreichte ein negativer Gleitkommawert sie nie.
+- **Beschreibung:** `readTopN` gab für jeden Nicht-`IntNode` `-1` (= alle) zurück. Jackson liest `2.0` als `DoubleNode`, also behielt `"top_n": 2.0` alle Ergebnisse und `"top_n": -1.5` wurde nicht abgewiesen. Upstream liest `json_value(body, "top_n", (int)documents.size())` (`server-context.cpp:5760`, b11639) über nlohmanns `get()`, das eine Gleitkommazahl per `static_cast` umwandelt: `2.0` und `2.7` behalten dort 2, `-1.0` lässt das `resize` in `format_response_rerank` scheitern. Ein Python-Client mit `top_n = len(docs) / 2` serialisiert `2.0`.
+- **Fix:** `readTopN` liest jede JSON-Zahl im `int`-Bereich (`isNumber() && canConvertToInt()`, dann `asInt()`, das wie upstreams Cast zur Null hin abschneidet); ein negativer Wert ist danach in beiden Schreibweisen eine ungültige Anfrage (400). Zwei bewusste Abweichungen stehen in der Javadoc: ein Wert, der keine Zahl ist (Zeichenkette, Boolean – upstream wandelt `true` in 1), und eine Zahl außerhalb des `int`-Bereichs (upstream: der Cast läuft über) behalten alle Ergebnisse.
+- **Tests:** `OaiRerankSupportTest#readTopNConvertsAFloatLikeUpstream` (`2.0` und `2.7` → 2), `#negativeFloatTopNIsAnInvalidRequest` (`-1.5`), `#readTopNKeepsAllForANonNumberOrAValueOutsideTheIntRange` (`"2"`, `true`, `4294967296` → -1, pinnt die dokumentierten Abweichungen).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot („Expected: is <2> but: was <-1>“ und „Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“), mit ihm grün (`OaiRerankSupportTest` 19/19, `OpenAiCompatServerHttpTest` 69/69); `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `5781ef5`
+
+
+### F231 – java-llama.cpp: `/v1/rerank`: ein explizites `"documents": null` neben `texts` wird abgewiesen statt aus `texts` bedient
+- **Zeit:** 2026-10-21 00:19 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OaiRerankSupport.java` (`readDocuments`)
+- **Schwere:** niedrig (still abweichend von upstream; ein Client muss `null` neben `texts` senden)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J6-4). Seit F111 (`texts` von text-embeddings-inference akzeptiert); der Kommentar behauptete Parität mit upstream.
+- **Beschreibung:** `readDocuments` wählte `request.has("documents") ? documents : texts`. Jacksons `has()` ist auch für einen expliziten `null`-Wert wahr, also nahm `{"documents":null,"texts":["a"]}` den `NullNode` und warf „'documents' (or TEI's 'texts') must be a non-empty array“ (400). Upstream liest `json_value(body, "documents", json_value(body, "texts", {}))` (`server-context.cpp:5753`, b11639), und `json_value` behandelt `null` wie „fehlt“ (`server-common.h:43-46`) – die Anfrage wird dort aus `texts` bedient.
+- **Fix:** `hasNonNull("documents")`; Kommentar und Javadoc nennen den Rückfall bei `null`. Die Formatwahl in `toResponse` (`request.has("texts")`) bleibt, weil upstreams `body.contains("texts")` ein `null` ebenfalls mitzählt. Ein `documents` falschen Typs bleibt ein 400 (upstream fiele still auf `texts` zurück; das wäre ein stiller Rückfall).
+- **Tests:** `OaiRerankSupportTest#readDocumentsFallsBackToTextsWhenDocumentsIsNull`, `#readDocumentsThrowsWhenDocumentsIsNullWithoutTexts`.
+- **Rot-Grün-Nachweis:** Ohne den Fix rot („Unexpected exception thrown: java.lang.IllegalArgumentException: 'documents' (or TEI's 'texts') must be a non-empty array“), mit ihm grün (`OaiRerankSupportTest` 21/21, `OpenAiCompatServerHttpTest` 69/69); `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `893ede3`
+
+
+### F232 – java-llama.cpp: `/v1/rerank`: ein nativer Antwortkörper, der kein JSON-Array ist, wird still zu einer leeren Ergebnisliste (200)
+- **Zeit:** 2026-10-21 00:35 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OaiRerankSupport.java` (`toTeiResponse`, `toOaiResponse`, neu `readNativeArray`), `LlamaModelBackend.java` (`rerank`)
+- **Schwere:** niedrig (in der Praxis nicht erreichbar: der native Körper ist immer `safe_json_to_str(rerank_results_to_json(...))`, ein Fehler im nativen Aufruf wird vorher zur `LlamaException`; ein Vertrags- und Konsistenzfix, kein für Nutzer sichtbarer Fehler)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J6-5). Der Rückfall in `toOaiResponse` (samt `toOaiResponseOnMalformedNativeBodyYieldsEmptyResults`, der ihn festschrieb) ist älter als der Branch; F111 hat ihn in das neue `toTeiResponse` kopiert, ohne Test.
+- **Beschreibung:** Beide Methoden fingen die `IOException` des Parsers ab und antworteten mit leeren Ergebnissen, ohne Log – für den Client ununterscheidbar von „nichts passt“. Zwei weitere stille Wege gingen nie durch den `catch`: Jackson liest einen leeren Körper als `MissingNode`, und ein Nicht-Array (`{}`) fiel durch `if (arr.isArray())`; beide ergaben ebenso `[]`. Der LangChain4j-Adapter (`LangChain4jMapping.parseRerankScores`) wirft im selben Fall.
+- **Fix:** Ein Helfer `readNativeArray` für beide Methoden: ein Parserfehler, ein leerer Körper und ein Nicht-Array werfen eine `IOException` mit einem Ausschnitt des Körpers (höchstens 80 Zeichen) und der Ursache. `IOException` statt der im Fund vorgeschlagenen `IllegalStateException`, weil `OpenAiBackend.rerank` genau diesen Typ für einen Serverfehler deklariert (`LlamaModelBackend.rerank` deklariert ihn jetzt) und SpotBugs die Umwandlung in eine ungeprüfte Ausnahme ohne Unterdrückung nicht hinnimmt (`EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS`). `completeNonStreaming` antwortet darauf mit 500 `server_error` und einer WARN-Zeile. `json.RerankResponseParser.parse(String)` (dokumentierter Rückfall der typisierten API, PIT-gesichert) bleibt unverändert.
+- **Tests:** `OaiRerankSupportTest#toOaiResponseRefusesANativeBodyThatIsNotAJsonArray` (ersetzt `#toOaiResponseOnMalformedNativeBodyYieldsEmptyResults`), `#toTeiResponseRefusesANativeBodyThatIsNotAJsonArray`; je `"not json"`, `""` und `"{}"`, und die Meldung zitiert den Körper.
+- **Rot-Grün-Nachweis:** Ohne den Fix rot („not json ==> Expected java.io.IOException to be thrown, but nothing was thrown.“, in beiden Tests); eine Zwischenfassung, die nur im `catch` warf, blieb für den leeren Körper rot („Expected java.lang.IllegalStateException to be thrown, but nothing was thrown.“) – die zusätzlichen Fälle pinnen also den ganzen Fix. Mit dem Fix grün (`OaiRerankSupportTest` 22/22, `OpenAiCompatServerHttpTest` 69/69, `LlamaArchitectureTest` 13/13); `spotless:apply` und `spotbugs:check` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `5c5a892`
+
+
+### F233 – java-llama.cpp: ein leeres oder unparsbares Tool-Schema geht als String an llama.cpp statt in Java abgewiesen zu werden
+- **Zeit:** 2026-10-21 01:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/ChatRequest.java` (`appendTool`, `buildToolsJson`, neu `parseSchemaOrThrow`)
+- **Schwere:** niedrig (die Anfrage scheiterte laut, aber spät und ohne Werkzeugnamen; nur ohne Grammatik still)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J8-2). Der String-Rückfall für unparsbaren Text ist älter als der Branch; F05 schickte ein Schema mit nachfolgendem Text in denselben Rückfall, F55 ein leeres oder blankes Schema (vorher `"parameters": null`, das upstream als „keine Parameter“ las).
+- **Beschreibung:** `buildToolsJson` schrieb ein Schema, das kein genau ein JSON-Wert war, als JSON-String unter `parameters`. Upstream (b11639) übernimmt den String als Schema (`common_chat_tools_parse_oaicompat`, `common/chat.cpp:580`; `common_chat_tool_parameters` ersetzt nur `null` und ein leeres Objekt, Zeile 546-554), und jeder Parser, der eine Tool-Grammatik baut, scheitert in `build_node` mit „schema must be an object“ (`common/json-schema.cpp:228`); `parse_oai_chat_params` macht daraus eine `InvalidRequestException`, deren Text weder das Werkzeug noch die Java-Ursache nennt. Der `force_pure_content`-Pfad (`chat.cpp:1375-1388`) rendert `"parameters":""` ohne Fehler in den Prompt. `JsonParameters.parseOrThrow` weist dieselbe Eingabeklasse mit einer `IllegalArgumentException` ab, der Branch behandelte denselben Fehler also zweifach.
+- **Fix:** `appendTool` liest das Schema mit dem strikten Reader (`FAIL_ON_TRAILING_TOKENS`) und wirft für einen Parserfehler und für `MissingNode` (leer, blank) `IllegalArgumentException("tool '': parameters must be exactly one well-formed JSON value, but was: ")`, Ausschnitt höchstens 80 Zeichen wie in `JsonParameters`, Ursache verkettet. `buildToolsJson` setzt nur noch den geparsten Knoten; beide String-Rückfälle sind weg. Ein Werkzeug ohne Parameter deklariert `{"type":"object","properties":{}}` (so wie `LangChain4jMapping.EMPTY_PARAMETERS_SCHEMA`), die Javadoc von `appendTool` sagt das. Nicht übernommen: `null` oder ein leeres Objekt für `""` einzusetzen (der zweite Vorschlag des Funds) wäre ein stiller Rückfall. SpotBugs meldet die Umwandlung der `IOException` (`EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS`); unterdrückt mit Begründung neben der gleichartigen von `JsonParameters.parseOrThrow`.
+- **Warum `chat_template_kwargs` anders bleibt:** `ParameterJsonSerializer.buildRawValueObject` (F203) schreibt einen Wert, der kein JSON ist, weiter als String-Literal. Das ist dort dokumentiert und ein gültiger Wert: eine Template-Variable darf eine Zeichenkette sein (etwa `reasoning_effort`), upstream reicht sie an das Template durch. Ein Tool-Schema muss dagegen ein Objekt sein, und einen String weist upstream ab; derselbe Rückfall erzeugt hier also nie eine gültige Anfrage.
+- **Tests:** `ChatRequestTest$JsonHelpers#appendToolRejectsASchemaThatIsNotExactlyOneJsonValue` (`""`, `"  "`, `"not json{{{"`, `{"type":"object"} {"required":["x"]}`, prüft die ganze Meldung mit dem Werkzeugnamen), `#appendToolBoundsTheRejectedSchemaInTheMessage` (300 Zeichen → 80 plus `...`); `#buildToolsJsonEmbedsAValidSchemaAsAnObject` prüft jetzt die genaue Ausgabe statt eines `containsString`. Ersetzt `#buildToolsJsonKeepsAnEmptySchemaAsTheStringFallbackInsteadOfNull` (F55) und den schwachen `#buildToolsJsonDoesNotSilentlyTruncateASchemaWithTrailingContent` (F05, prüfte nur `not(containsString(...))`).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot („Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“, in allen vier Fällen und im Längentest), mit ihm grün (`ChatRequestTest` 28/28, `ChatResponseTest` 8/8, `LlamaArchitectureTest` 13/13, `LangChain4jMappingTest` 23/23); `spotless:apply`, `spotbugs:check` und `javadoc:javadoc` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `0a49c54`
+
+
+### F234 – java-llama.cpp: `appendCsvValue` überschreibt einen leeren Vorwert still statt ihn zu ergänzen
+- **Zeit:** 2026-10-21 01:14 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/CliParameters.java` (`appendCsvValue`), `ModelParameters.java` (Javadoc von `addLoraAdapter`, `addControlVector`)
+- **Schwere:** niedrig (ein ungültiger, leerer Adapterpfad verschwand ohne Meldung)
+- **Historie:** Gefunden beim Review des Branches am 2026-10-20 (Fund J8-7). Eingeführt mit F188 (`appendCsvValue`); dessen Eintrag nennt keinen Grund für den `isEmpty()`-Zweig, und seine Tests nutzten nie einen leeren Wert.
+- **Beschreibung:** `appendCsvValue` schrieb `present == null || present.isEmpty() ? field : present + "," + field`. Ein leerer Vorwert kann nur aus einem früheren `addLoraAdapter("")` oder `addControlVector("")` stammen (die skalierten Varianten schreiben immer `FNAME:SCALE`), und der nächste Aufruf überschrieb ihn: `addLoraAdapter("").addLoraAdapter("b.gguf")` ergab `--lora b.gguf`. In anderer Reihenfolge (`b.gguf,`) oder allein (`--lora ""`) kam der leere Pfad dagegen bei upstream an, dessen `parse_csv_row` (`common/arg.cpp:1369-1410`, b11639) leere Felder behält und dessen `--lora`-Handler daraus einen Adapter macht, der beim Laden laut scheitert. Für einen fehlenden Schlüssel ist `present` ohnehin `null`; der Zweig wirkte nur als dieser stille Verlust.
+- **Fix:** `appendCsvValue` weist einen leeren Wert mit `IllegalArgumentException("`) und `CApi.ARawCompletionWithMessagesAndATemplateWithoutThinkingTagsIgnoresTheBudget` (Template ohne Tags: mit und ohne Budget derselbe `content`, kein Fehler). Java `ChatAdvancedTest#testReasoningBudgetBesideMessagesInCompleteAndGenerateForcesTheEndTag` (das Reasoning-Modell mit demselben Template über `setChatTemplate`, `withMessages` + `withReasoningBudgetTokens(1)` + `withReturnTokens(true)`: das erste erzeugte Token von `completeWithStats` ist das erste Token von `encode("")`, `generate()` liefert denselben Text; Token statt Text, weil ein spezielles End-Tag als leerer Text gerendert wird, und das Reasoning-Modell, weil ein SentencePiece-Tokenizer einem allein tokenisierten Tag ein Leerzeichen voranstellt und das Budget dann auf keinem Weg anspringt). ctest 657/657; `ChatAdvancedTest` 19/19 und `InferenceParametersTest` 114/114 mit dem Winzmodell; `spotbugs:check` und Javadoc grün.
+- **Rot-Grün-Nachweis:** Vor dem Fix rot: `ARawCompletionWithMessagesKeepsTheReasoningBudgetOfTheChatTemplate` mit „Expected equality of these values: blocking.rfind("", 0) Which is: 18446744073709551615“ und „blocking.substr(blocking.size() - std::min(blocking.size(), answer.size())) Which is: "FFFF"“; Java mit „expected: <60> but was: <55>“ (`content` „7%j%j%j%j%“, `generation_prompt` „“). Mit dem Fix grün; ohne die `json_schema`-Ausnahme in `prime_reasoning_budget` rot mit „jllama_completions(model, with_schema.dump().c_str(), &constrained.s) Which is: 2“. `ARawCompletionWithMessagesAndATemplateWithoutThinkingTagsIgnoresTheBudget` pinnt das unveränderte Verhalten und war vorher wie nachher grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `85f6afa`
+- **Nachtrag (Branch-Review 2026-10-21, Abschluss):** Offen und in `TODO.md` eingetragen: der Rohweg übernimmt neben den Budget-Feldern nicht die `preserved_tokens` der Session, deshalb erscheint ein End-Tag, das das Vokabular als ein spezielles Token führt, im `content` als leerer Text (das Budget greift trotzdem; der Java-Test prüft deshalb Token). `preserved_tokens` ist ein Schema-Feld; die Übernahme braucht einen eigenen Rot-Grün-Test. CHANGELOG ([Unreleased], Fixed und „Upgrading from 5.1“ Punkt 14) beschreibt den Fix. Reine Doku.
+- **Nachtrag (Branch-Review 2026-10-21, Abschluss vor 6.0.0):** Der offene Rest ist mit F250 erledigt: der Rohweg übernimmt die `preserved_tokens` der Chat-Sitzung, das erzwungene End-Tag steht auch bei einem speziellen Token als Text im `content`; `ChatAdvancedTest#testReasoningBudgetBesideMessagesInCompleteAndGenerateForcesTheEndTag` prüft jetzt zusätzlich den Text (in CI mit Qwen3, dessen `` ein spezielles Token ist). `TODO.md`-Eintrag entfernt.
+- **Nachtrag (PR #554, 2026-10-21):** `ChatAdvancedTest#testReasoningBudgetBesideMessagesInCompleteAndGenerateForcesTheEndTag` war im Windows-x86-64-Java-Job des PR rot: blockierend „\n\n2 + 2 = 4.“, gestreamt „\n\nThe question …“ aus demselben Request. Ursache ist der Prompt-Cache: die zweite Generierung übernimmt den Prompt der ersten aus dem Slot und wertet nur dessen letztes Token neu aus, ein Batch von einem Token gegen den Batch des ganzen Prompts, und die Matrixmultiplikationen der beiden Batch-Formen runden so verschieden, dass ein Fast-Gleichstand unter Greedy-Sampling kippt (unter Linux nicht, dessen Runner ein anderes CPU-Modul nimmt). Der Request setzt `cache_prompt` jetzt auf `false`, so sind beide Texte dieselbe Rechnung. Commit java-llama.cpp `be90df1`.
+
+
+### F250 – java-llama.cpp: Rohweg mit `messages` übernimmt die `preserved_tokens` der Chat-Sitzung nicht, ein spezielles End-Tag erscheint als leerer Text
+- **Zeit:** 2026-10-21 13:33 UTC
+- **Datei:** `llama/src/main/cpp/jllama_engine.cpp` (`render_messages_into_prompt`, `prepare_raw_completion`, `completions`, `completions_stream_request`; `prime_reasoning_budget` wird zu `apply_raw_chat_sampling`), `llama/src/main/cpp/jllama_engine.hpp` (`raw_chat_sampling` statt des Parameters `generation_prompt` von `blocking_completion`/`streaming_request`), Kommentar zu `jllama_completions` in `jllama_api.h`, `docs/c-api.md`; Testfixture `llama/src/test/cpp/tiny_llama_gguf.hpp` (optionale Spezial-Tokens)
+- **Schwere:** niedrig (der Text des Rohwegs verliert ein Tag, das Budget selbst greift)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md; Brief JR-1), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. Rest von F249 (Nachtrag dort).
+- **Beschreibung:** Bei b11639 gehören `preserved_tokens` der Chat-Sitzung: `oaicompat_chat_params_parse` entfernt das Feld aus dem Request (`server-common.cpp:1477-1480`), `common_chat_session` löst die Strings des Templates (Thinking-Tags u. a., `chat-diff-analyzer.cpp` `collect_preserved_tokens`) in Token-Ids auf, und `server_task::apply_chat_session` → `apply_sampling` fügt sie dem Task hinzu. `accept_special_token` (`server-context.cpp:4332`) rendert ein Kontroll-Token nur dann als Text, wenn `params.special` gilt oder es zu den `preserved_tokens` gehört. Der Rohweg mit `messages` postet keine Sitzung und übernahm seit F249 die Budget-Felder und den Generation-Prompt, nicht aber die erhaltenen Token: ein End-Tag, das das Vokabular als ein spezielles Token führt (Qwen3 ``), wurde vom Budget erzwungen und als leeres Stück gerendert.
+- **Fix:** `render_messages_into_prompt` gibt eine `raw_chat_sampling` zurück: die `preserved_tokens`, die `session.apply_sampling` einem leeren `common_params_sampling` gibt (immer, auch ohne Thinking-Tags), und wie bisher den Generation-Prompt, wenn das Budget übernommen wird. `apply_raw_chat_sampling` setzt den Prompt wie zuvor `prime_reasoning_budget` und fügt die Token den `preserved_tokens` hinzu, die das Schema aus dem Request schon eingetragen hat (Vereinigung). Abweichung vom Chat-Weg, bewusst: dort verwirft der Parser die `preserved_tokens` des Requests, weil die Sitzung sie besitzt; auf dem Rohweg ist das Feld ein eigenes Completion-Feld und bleibt deshalb neben denen des Templates wirksam, statt still verworfen zu werden. Abweichung vom Brief: die Sitzung führt Token-Ids, keine Strings, und das Parse-Ergebnis enthält das Feld nicht; die Ids werden deshalb direkt in den Task gelegt statt als Strings ins Request-JSON.
+- **Tests:** C++ `CApiReasoningBudget.ARawCompletionWithMessagesRendersTheTemplatesSpecialTagsAsText` (das Zufallsmodell mit ``, `` und `` als Kontroll-Token — die Fixture `write_tiny_llama_gguf` nimmt dafür eine optionale Liste —, das Thinking-Template aus F249, Budget 1, `ignore_eos`, Temperatur 0: der Chat-Weg als Bezug ohne `` in der Antwort, der Rohweg blockierend mit `content`, der mit `` beginnt, gestreamt derselbe Text; mit einem `logit_bias` auf `` ist der `content` ohne eigene `preserved_tokens` genau ``, mit `"preserved_tokens": [""]` genau `` und elf ``). Java `ChatAdvancedTest#testReasoningBudgetBesideMessagesInCompleteAndGenerateForcesTheEndTag` prüft zusätzlich, dass der Text mit `` beginnt — rot vor dem Fix nur in CI mit Qwen3 (spezielles Token), lokal mit dem Byte-Tokenizer des Winzmodells in beiden Fällen grün. ctest `CApi|Lora` 68/68, `ChatAdvancedTest` 19/19 mit dem Winzmodell.
+- **Rot-Grün-Nachweis:** Vor dem Fix rot: „Expected equality of these values: blocking.rfind("", 0) Which is: 18446744073709551615“ (`content` „,''''''''''“, elf Zeichen bei zwölf Token), „json::parse(unpreserved.str()).at("content").get() Which is: """ und „Which is: "…" expected Which is: "…"“. Mit dem Fix grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `d88214f`
+
+
+### F251 – java-llama.cpp: die C-API nimmt den Slot-Dateinamen als Pfad, `--slot-save-path` wirkt nicht
+- **Zeit:** 2026-10-21 13:45 UTC
+- **Datei:** `llama/src/main/cpp/jllama_engine.cpp` (`slot_action`), `jllama_engine.hpp`, `jllama_api.h` (`jllama_slot_op`, `jllama_slot_action`), `LlamaModel.saveSlot`/`restoreSlot` (Parameter `filename`, Javadoc), `Session.save`/`restore`/`checkpoint`/`fork` (Parameter `filename`), `value.SessionCheckpoint` (`getFilepath` → `getFilename`), `llama-ffm` `FfmLlamaModel.slotAction` (Javadoc), `docs/c-api.md`, `README.md`
+- **Schwere:** niedrig bis mittel (eine Option ohne Wirkung; eine Slot-Datei landet relativ zum Arbeitsverzeichnis bzw. überall, wohin der Prozess schreiben darf)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md, Abschnitt Tests; Brief JR-12), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. Beim Review von F240 aufgefallen: `LoraCacheFreshSlot` schrieb `slot0.bin` ins Arbeitsverzeichnis, die Fixture übergab deshalb einen Pfad unter ihrem Save-Verzeichnis.
+- **Beschreibung:** Upstreams Route `POST /slots/:id?action=save|restore` (b11639, `server-context.cpp:5333-5357`, `handle_slots_save`/`handle_slots_restore` `:5957-6011`) lehnt ohne `slot_save_path` ab (501 „This server does not support slots action. Start it with `--slot-save-path`“), prüft den Namen mit `fs_validate_filename` (kein `/` oder `\`, kein `..`, nicht leer; sonst 400 „Invalid filename“) und setzt `filepath = slot_save_path + filename`. `engine::slot_action` setzte `filepath = filename`: `--slot-save-path` (`ModelParameters.setSlotSavePath`) hatte keine Wirkung, ein Name wie `../x.bin` oder ein absoluter Pfad wurde geschrieben, und ohne Verzeichnis wurde trotzdem gespeichert.
+- **Fix:** `slot_action` folgt der Route: ohne `--slot-save-path` werfen Save und Restore `std::runtime_error` („Saving or restoring a slot needs a slot directory: load the model with --slot-save-path  (ModelParameters.setSlotSavePath)“ → `LlamaException`/`JLLAMA_STATUS_ERROR`, wie upstream vor der Namensprüfung), ein Name, den `fs_validate_filename` ablehnt, ist `jllama_invalid_request` („Invalid slot file name '…': name a file inside the --slot-save-path directory, without a directory part“ → `InvalidRequestException`/`JLLAMA_STATUS_INVALID_REQUEST`), und der Pfad ist `params.slot_save_path + filename` (der Parser hängt den Trenner an). Erase braucht keine Datei und bleibt ohne Verzeichnis verfügbar — Abweichung von upstream, das die ganze Route sperrt, weil `eraseSlot` in der Bibliothek ohne Dateibezug nützlich ist. Java: Parameter heißen `filename`, `SessionCheckpoint.getFilename()` statt `getFilepath()` (keine Rückwärtskompatibilität nötig, 6.0.0); Javadoc nennt Verzeichnis, Validierung und beide Ausnahmen.
+- **Tests:** C++ `CApiSlotFiles.ASlotFileIsNamedInsideTheSlotSaveDirectory` (Modell mit `--slot-save-path`: `slot0.bin` landet im Verzeichnis, nicht im Arbeitsverzeichnis, und lässt sich wiederherstellen; `../escaped.bin`, `sub/slot.bin`, ein absoluter Pfad, `..\escaped.bin` und `""` sind für Save und Restore `INVALID_REQUEST` mit der Meldung, nichts wird geschrieben), `CApi.SlotActionsAndLoraAdapters` (ohne Verzeichnis: Save und Restore `JLLAMA_STATUS_ERROR` mit der Meldung, Erase geht), `LoraCacheFreshSlot` auf Dateinamen umgestellt. Java `ChatScenarioTest#testSaveAndRestoreSlot` (Modell mit `setSlotSavePath(@TempDir)`: Datei im Verzeichnis, nicht im Arbeitsverzeichnis, Restore, `../escaped.bin` und ein Pfad → `InvalidRequestException`; vorher nachsichtig „if exists“), `ErrorHandlingTest#testSaveSlotWithoutSlotSavePathIsRefused` (`LlamaException`, keine `InvalidRequestException`, Meldung nennt `--slot-save-path`), `SessionForkRewindIntegrationTest` (Modell mit Slot-Verzeichnis, Checkpoint- und Fork-Dateinamen), `SessionCheckpointTest`, `SessionConcurrencyTest`; FFM `FfmLlamaModelIntegrationTest#slotEraseAnswersAndASaveWithoutASlotDirectoryIsRefused` und `#aSlotFileIsANameInsideTheSlotDirectory`. ctest `CApiSlotFiles|SlotActionsAndLora|LoraCacheFreshSlot` 3/3; Java mit dem Winzmodell `ChatScenarioTest` 19 von 20 (`testChatCompleteMultiTurnThreeTurns` braucht mehr als 128 Token Kontext, siehe `TODO.md`), `ErrorHandlingTest` 23/23, `SessionCheckpointTest` 5/5, `SessionTest` 3/3, mit einer 4096-Token-Variante des Winzmodells `SessionForkRewindIntegrationTest` 3/3 und `SessionConcurrencyTest` 4/4; `llama-ffm` verify 38/38.
+- **Rot-Grün-Nachweis:** Vor dem Fix rot (C++): `CApiSlotFiles` mit „std::filesystem::exists(dir / "slot0.bin") Actual: false Expected: true“ und „jllama_slot_action(model, JLLAMA_SLOT_SAVE, 0, name.c_str(), &refused.s) Which is: 0 JLLAMA_STATUS_INVALID_REQUEST“, `SlotActionsAndLoraAdapters` mit „jllama_slot_action(model_, JLLAMA_SLOT_SAVE, 0, "slot0.bin", &refused.s) Which is: 0 JLLAMA_STATUS_ERROR Which is: 1“; Java gegen die alte Bibliothek: `ChatScenarioTest.testSaveAndRestoreSlot:604 {"id_slot":0,"filename":"chat-scenario-slot.bin",…} ==> expected:  but was: ` und `ErrorHandlingTest.testSaveSlotWithoutSlotSavePathIsRefused:399 Expected net.ladenthin.llama.exception.LlamaException to be thrown, but nothing was thrown.` Mit dem Fix grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `a1a5efb`
+- **Nachtrag (Branch-Review 2026-10-21, Prüfung):** Das Beispiel „Conversation checkpoints“ im README übergab noch Pfade (`checkpoints/turn1.bin`, `checkpoints/branch.bin`), die seit dem Fix als `InvalidRequestException` abgelehnt werden; es nennt jetzt das Slot-Verzeichnis (`setSlotSavePath`) und Dateinamen darin.
+- **Nachtrag (Branch-Review 2026-10-21, Brief JR-22):** Folgepunkt des Prüfers aus dem Durchgang 2026-10-21 (Brief JR-22), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. Die Slot-Datei-Tests waren nicht hermetisch: `ErrorHandlingTest#testSaveSlotWithoutSlotSavePathIsRefused` prüfte, dass `slot.bin` im Arbeitsverzeichnis fehlt (eine fremde Datei dort machte ihn rot), `ChatScenarioTest#testSaveAndRestoreSlot` und `FfmLlamaModelIntegrationTest#aSlotFileIsANameInsideTheSlotDirectory` prüften feste Namen (`chat-scenario-slot.bin`, `escaped.bin` neben dem Temp-Verzeichnis) — der Java-Test löschte im `finally` sogar eine fremde Datei dieses Namens. Jetzt mit Namen je Lauf (`slot-.bin`, `chat-scenario-slot-.bin`, `escaped-.bin`); `ErrorHandlingTest` prüft die genaue Meldung („Saving or restoring a slot needs a slot directory: load the model with --slot-save-path  (ModelParameters.setSlotSavePath)“, kein `InvalidRequestException`) für den bloßen Namen, einen absoluten Pfad in einem `@TempDir` und `../` beim Wiederherstellen, und dass weder im Arbeitsverzeichnis noch im `@TempDir` eine Datei entsteht. Reine Test-Härtung, kein Produktionscode; grün mit dem Winzmodell: `ErrorHandlingTest` 23 (2 übersprungen, FIM), `ChatScenarioTest#testSaveAndRestoreSlot`, `FfmLlamaModelIntegrationTest` 18/18.
+
+
+### F252 – java-llama.cpp: die Tag-Felder des Reasoning-Budgets fehlen im Builder, und ohne `reasoning_budget_message` erzwingt das Budget nichts
+- **Zeit:** 2026-10-21 13:58 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/parameters/RequestField.java` (`REASONING_BUDGET_START_TAG`, `REASONING_BUDGET_END_TAGS`, `REASONING_BUDGET_MESSAGE`), `InferenceParameters` (`withReasoningBudgetStartTag`, `withReasoningBudgetEndTags`, `withReasoningBudgetMessage`; Javadoc von `withReasoningBudgetTokens`), `llama/src/main/cpp/jllama_engine.cpp` (`populate_completion_task`), Kommentar zu `jllama_completions` in `jllama_api.h`, `docs/c-api.md`
+- **Schwere:** niedrig bis mittel (ein Budget wird verlangt und nicht durchgesetzt, ohne Meldung)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (Javadoc von `withReasoningBudgetTokens` seit F249, Report-Zeile 13a; Brief JR-14), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen.
+- **Beschreibung:** Upstreams Completion-Schema (b11639, `server-schema.cpp:332-379`) liest `reasoning_budget_tokens`, `reasoning_budget_start_tag`, `reasoning_budget_end_tags` (Alias `reasoning_budget_end_tag`), `reasoning_budget_message` und `reasoning_control`. Der Builder bot nur `reasoning_budget_tokens` an; auf `complete()`/`generate()` ohne `messages` hatte `withReasoningBudgetTokens` deshalb keine Wirkung (seit F249 so dokumentiert). Zweiter Teil, beim Bau des Tests gemessen: die Sequenz, die das Budget erzwingt (`reasoning_budget_forced`), setzt das Schema nur im Handler von `reasoning_budget_message` (`server-schema.cpp:367-379`), und `field_str::eval` ruft den Handler nur, wenn das Feld da ist. Ein Request mit Tags, aber ohne Nachricht, armierte also einen Budget-Sampler, der bei aufgebrauchtem Budget nichts erzwang (`FORCING` mit leerer Sequenz geht sofort nach `DONE`). Upstreams Chat-Weg setzt die Nachricht immer (`server-common.cpp:1451`, Default `params_base.sampling.reasoning_budget_message`), dort tritt das nicht auf; upstreams `/completion` hat dieselbe Lücke (Upstream-Kandidat).
+- **Fix:** Drei `SCHEMA`-Konstanten und drei Builder-Methoden: `withReasoningBudgetStartTag(String)` (nicht leer), `withReasoningBudgetEndTags(String...)` (mindestens einer, keiner leer; sonst `IllegalArgumentException` statt eines Budgets ohne Ende), `withReasoningBudgetMessage(String)` (leer erzwingt das bloße End-Tag). Die Javadoc nennt den Geltungsbereich (Requests ohne `messages`; mit `messages` und auf dem Chat-Weg gewinnen die Thinking-Tags des Templates, wenn es welche hat), dass das Budget erst bei einem *erzeugten* Start-Tag zu zählen beginnt (ein Tag am Ende des Prompts sieht es nicht; dafür `messages`), und dass ein spezielles Tag nur mit `preserved_tokens` als Text erscheint, mit Beispiel. `populate_completion_task` setzt nach dem Schema, wenn End-Tags da sind und die erzwungene Sequenz leer ist, sie auf das erste End-Tag mit der Default-Nachricht des Servers davor — wie der Chat-Weg. `reasoning_control` bekommt bewusst keine Builder-Methode: es armiert den Sampler nur für die Laufzeit-Aktion `reasoning_end` (`SERVER_TASK_TYPE_CONTROL`, `server-context.cpp:2847-2876`), die die Bibliothek nicht anbietet; die Methode wäre wirkungslos. Abweichung vom Brief: der Java-Test kann den Prompt `"\n"` nicht nutzen, weil ein Rohtask ohne `messages` keinen Generation-Prompt hat und das Budget ein Start-Tag im Prompt nicht sieht (wie upstream); er lässt das Modell das Start-Tag per Logit-Bias selbst schreiben. Dazu kam der zweite Teil (Nachricht), ohne den die neuen Methoden ohne `withReasoningBudgetMessage` wirkungslos geblieben wären.
+- **Tests:** Java `InferenceParametersTest` (`testSetReasoningBudgetStartTag`, `testSetReasoningBudgetEndTags`, `testSetReasoningBudgetMessage`, `testAnEmptyMessageForcesTheBareEndTag`, `testReasoningBudgetTagsMustNotBeEmpty`, `testReasoningBudgetTagWithersReturnNewInstance`), `WireNameRegistryTest` (erreicht jede neue Konstante über einen Builder), `ChatAdvancedTest#testReasoningBudgetTagsWithoutMessagesForceTheEndTag` (modellunabhängig: das erste Token von `encode("Hello")` per Logit-Bias +100 wiederholt, sein Text ist das Start-Tag; Budget 0, End-Tag `END`, `ignore_eos`, Temperatur 0: `complete()` beginnt mit Start-Tag + `END`, `generate()` liefert denselben Text, ohne Tags kein `END`). C++ `CApiReasoningBudget.ARawCompletionWithoutMessagesForcesTheEndTagOfItsOwnTags` (das Zufallsmodell mit ``/`` als Kontroll-Token, Logit-Bias auf ``, `preserved_tokens`, Budget 0, ohne Nachricht: `content` „“; mit `"reasoning_budget_message": "!"` „!“) und die Vertragstests `JavaRequestFieldContract.*` nach einem Reconfigure (die neuen Felder liest das Schema). ctest `CApiReasoningBudget|CApi\.` 31/31, `Java` 13/13; `InferenceParametersTest` 120/120, `WireNameRegistryTest` 8/8, `ChatAdvancedTest` 20/20 mit dem Winzmodell; Javadoc grün.
+- **Rot-Grün-Nachweis:** Vor dem Fix rot: Java kompilierte nicht („InferenceParametersTest.java:[445,65] cannot find symbol“, ebenso `ChatAdvancedTest.java:[380,21]`); mit den Builder-Methoden, aber gegen die Bibliothek ohne den Engine-Teil, `ChatAdvancedTest.testReasoningBudgetTagsWithoutMessagesForceTheEndTag:388 HHHHHHHHHH ==> expected:  but was: `; C++ „json::parse(without_message.str()).at("content").get() Which is: ""“. Mit beiden Teilen grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `fdca13a`
+
+
+### F253 – java-llama.cpp: Zahlenfelder mit falschem Typ werden auf den Chat-Routen still übergangen (`max_tokens: "100"` = ohne Obergrenze)
+- **Zeit:** 2026-10-21 14:28 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java` (neu `optionalNumber`; `applyCommonFields`, `applyLogprobs`, `readMaxTokens`, `applyReasoningControls`, `withInt`, `withFloat`), `AnthropicApiSupport.java` (`toOpenAiChatRequest`, `copyNumber`), `ResponsesApiSupport.java` (`copyNumber`), `OllamaApiSupport.java` (`copyNumber`, `copyNumPredict`, `copyLlamaCppOptions`), `README.md`
+- **Schwere:** niedrig bis mittel (ein Client mit einem falsch typisierten Feld bekam ohne Hinweis eine andere Anfrage: `"max_tokens": "100"` lief ohne Obergrenze, `"n": "2"` mit einer Antwort, `"n_probs": "3"` ohne Wahrscheinlichkeiten, eine String-Temperatur mit der Vorgabe des Servers)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md „Chat mapper“, erster und zweiter Punkt; Brief JR-4), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. F223 hatte dieselbe Klasse für Anthropics `max_tokens` vor dem Mapper geschlossen und den OpenAI-Weg als eigenen Fund vermerkt.
+- **Beschreibung:** Der Mapper las jedes Zahlenfeld mit `isNumber()`/`isInt()`/`isIntegralNumber()` und ließ alles andere still weg; `readMaxTokens` lieferte dann -1 (unbegrenzt), `n` prüfte nur Zahlen, `n_probs` nur `int` (ein `3.0` fiel weg), `repeat_last_n`, `min_keep`, `n_keep`, `n_cache_reuse` nur ganze Zahlen (ein `32.7` fiel weg). upstreams Schema (`field_num::eval`, `server-schema.cpp:531-549`, b11639) liest den ersten vorhandenen Namen eines Felds mit `get()`: ein String, ein Objekt oder ein Array ist `400` „Field 'max_tokens': … type must be number, but is string“, `null` gilt als fehlend (`has_value`), ein Bruch wird in ein Ganzzahlfeld abgeschnitten. Dieselbe stille Klasse hatten die Übersetzungen davor: Anthropics `temperature`/`top_p`/`top_k` (upstream reicht sie durch, das Schema weist ab), Responses' `temperature`/`top_p`/`max_output_tokens` (upstream kopiert den ganzen Body) und Ollamas `options` (Ollama weist einen falsch typisierten Wert ab).
+- **Fix:** Ein gemeinsamer Leser `OpenAiRequestMapper.optionalNumber(node, field)`: fehlend oder `null` → `null` (Vorgabe), eine Zahl → der Wert, alles andere → `IllegalArgumentException` mit upstreams Text ohne nlohmanns Präfix („Field 'max_tokens': type must be number, but is string“). Er gilt für jedes Zahlenfeld des Mappers (`n_predict`/`max_completion_tokens`/`max_tokens` in upstreams Reihenfolge, der erste vorhandene zählt; `n`, `n_probs`, `n_cache_reuse`, `id_slot`, `temperature`, `top_p`, `top_k`, `seed`, die Strafen, die llama.cpp-Sampling-Felder, `top_logprobs` neben `logprobs: true`, `reasoning_budget_tokens`/`thinking_budget_tokens`); Ganzzahlfelder schneiden einen Bruch ab wie upstream. Anthropic liest `max_tokens` (F223) und `temperature`/`top_p`/`top_k`, Responses `temperature`/`top_p`/`max_output_tokens`, Ollama jede Zahl-Option (auch auf `/api/generate` und dem FIM-Weg) über denselben Leser; die Anthropic-eigene Meldung von F223 entfällt. Bewusst strenger als upstream, im README genannt: ein Boolean wird abgewiesen (nlohmann liest ihn als 0/1), ebenso ein falsch typisiertes `top_logprobs`, `id_slot` und Reasoning-Budget, wo upstream (`json_value`) eine Warnung loggt und die Vorgabe nimmt. Abweichung vom Brief: Responses und Ollama mit erfasst, weil dieselbe stille Klasse sonst auf zwei Chat-Routen offen geblieben wäre.
+- **Tests:** `OpenAiRequestMapperTest#aStringMaxTokensIsRefusedRatherThanReadAsNoLimit`, `#everyNumberFieldThatIsNotANumberIsRefusedLikeUpstreamsSchema` (26 Felder × String/Boolean/Objekt/Array, Meldung wörtlich), `#aTopLogprobsThatIsNotANumberIsRefused`, `#aNullNumberFieldIsTheDefaultLikeUpstream`, `#aFractionalIntegerFieldIsTruncatedLikeUpstream`, `#onlyTheFirstPresentTokenCapAliasIsReadLikeUpstream`; `AnthropicApiSupportTest#aSamplingFieldThatIsNotANumberIsRefused`; `ResponsesApiSupportTest#aSamplingFieldThatIsNotANumberIsRefused`; `OllamaApiSupportTest#anOptionThatIsNotANumberIsRefused` (12 Optionen); `OpenAiCompatServerHttpTest#aStringMaxTokensOnTheChatRouteIsA400` (neues `MappingBackend`, das wie `LlamaModelBackend` erst den echten Mapper aufruft; gestreamt und blockierend 400 `invalid_request_error` mit der Meldung, nichts generiert).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot (8 Fehlschläge): „OpenAiRequestMapperTest.aStringMaxTokensIsRefusedRatherThanReadAsNoLimit:543 Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“, „{"messages":[{"role":"user","content":"hi"}],"n_predict":"3"} ==> Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“, „aFractionalIntegerFieldIsTruncatedLikeUpstream:597 Expected: is <32>“, je „temperature ==> Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“ in `AnthropicApiSupportTest`, `ResponsesApiSupportTest`, `OllamaApiSupportTest`, und „OpenAiCompatServerHttpTest.aStringMaxTokensOnTheChatRouteIsA400:943 … Expected: is <400>“ (200); mit dem Fix grün (`OpenAiRequestMapperTest` 53/53, `AnthropicApiSupportTest` 38/38, `ResponsesApiSupportTest` 69/69, `OllamaApiSupportTest` 49/49, `OpenAiCompatServerHttpTest` 71/71, `LlamaModelBackendTest` 4/4, `AnthropicStreamTranslatorTest` 11/11). `spotless:apply` grün; `spotbugs:check` am Ende des Batches meldete zweimal `PRMC_POSSIBLY_REDUNDANT_METHOD_CALLS` (`choices.asInt()`, `slotId.asInt()`) und einmal `SACM_STATIC_ARRAY_CREATED_IN_METHOD` (die Alias-Liste in `readMaxTokens`), im Folge-Commit behoben, danach grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `5abe67e`
+
+
+### F254 – java-llama.cpp: Anthropics benanntes `tool_choice` lässt jedes Werkzeug zu, ein `tool_choice`-Objekt anderen Typs läuft still als `auto`
+- **Zeit:** 2026-10-21 14:35 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java` (`toOpenAiChatRequest`, `toOpenAiToolChoice`), `OpenAiRequestMapper.java` (`namedToolChoice`, Klassen-Javadoc), `ResponsesApiSupport.java` (Kommentar), `README.md`
+- **Schwere:** niedrig (mit mehreren Werkzeugen durfte das Modell statt des verlangten ein anderes aufrufen; ein `allowed_tools`/`mcp`/`custom`-`tool_choice` lief ohne die verlangte Einschränkung)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md „Chat mapper“, vierter Punkt; Brief JR-5), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. F227 hatte die benannte Funktion auf dem Chat- und dem Responses-Weg eingeengt und beide Reste ausdrücklich ausgenommen; F107 bildete Anthropics `none` ab.
+- **Beschreibung:** `AnthropicApiSupport.toOpenAiToolChoice` machte `{"type":"tool","name":"g"}` zu `"required"` über alle Werkzeuge („OpenAI's textual tool_choice cannot name a specific function“), wie upstream (`convert_anthropic_to_oai`, `server-chat.cpp:567-578`, b11639); jeder andere Typ ergab `null`, die Anfrage lief als `auto`. Auf dem Chat- und dem Responses-Weg fiel ein `tool_choice`-Objekt, dessen Typ nicht `function` ist (`allowed_tools`, `custom`, `mcp`, ein gehostetes Werkzeug), ebenso still weg; upstream liest einen Nicht-String als `auto` (`json_value(body, "tool_choice", "auto")`, `server-common.cpp:1282`). llama.cpp kennt nur `auto`/`none`/`required` (`common/chat.cpp:314-325`).
+- **Fix:** Anthropic: `{"type":"tool","name":…}` wird OpenAIs benanntes Objekt `{"type":"function","function":{"name":…}}`, das der Mapper nach F227 durchsetzt (nur dieses Werkzeug, `required`; ein unbekannter Name ist „tool_choice names unknown tool 'g'“); ohne Namen „tool_choice of type 'tool' needs the tool's 'name'“; `auto`/`any`/`none` wie bisher; jeder andere Typ „tool_choice type 'allowed_tools' is not supported; use auto, any, tool or none“, ein Wert, der kein Objekt ist, „'tool_choice' must be an object (got …)“; `disable_parallel_tool_use` bleibt. Chat und Responses: `namedToolChoice` lässt fehlend, `null` und Strings durch (einen unbekannten String weist upstreams Parser ab), weist ein Objekt jedes anderen Typs als `function` mit „tool_choice type '…' is not supported; use auto, none, required or a function“ und jeden anderen JSON-Typ mit „'tool_choice' must be a string or an object (got …)“ ab. Alles als `IllegalArgumentException` → 400 (Anthropic: `invalid_request_error`, gestreamt wie blockierend vor dem Strom). Entscheidung: 400 statt dokumentiertem Rückfall, weil die verlangte Einschränkung sonst still entfällt. Wie bisher geprüft nur, wenn die Anfrage Werkzeuge trägt.
+- **Tests:** `AnthropicApiSupportTest#toolChoiceNamingAToolForcesThatTool` (zwei Werkzeuge; das übersetzte Request trägt das benannte Objekt, der echte Mapper reicht nur `g` mit `required` weiter, `disable_parallel_tool_use` bleibt), `#toolChoiceToolWithoutANameIsRefused`, `#toolChoiceOfAnotherTypeIsRefused` (`allowed_tools`, `mcp`, leer), `#toolChoiceThatIsNoObjectIsRefused`; `OpenAiRequestMapperTest#aToolChoiceObjectOfAnotherTypeIsRefused`, `#aToolChoiceThatIsNeitherAStringNorAnObjectIsRefused`; `ResponsesApiSupportTest#aToolChoiceObjectOfAnotherTypeIsRefused` (`allowed_tools`, `mcp`, `file_search`, `custom`); `OpenAiCompatServerHttpTest#anUnsupportedAnthropicToolChoiceIsA400` (über das `MappingBackend` aus F253: `allowed_tools` und ein unbekannter Werkzeugname, gestreamt und blockierend 400, nichts generiert).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot (8 Fehlschläge): „AnthropicApiSupportTest.toolChoiceNamingAToolForcesThatTool:483 Expected: is "function" but: was """, „toolChoiceOfAnotherTypeIsRefused:510 allowed_tools ==> Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“, ebenso `toolChoiceToolWithoutANameIsRefused`, `toolChoiceThatIsNoObjectIsRefused`, die beiden Mapper- und der Responses-Test, und „OpenAiCompatServerHttpTest.anUnsupportedAnthropicToolChoiceIsA400:972 … Expected: is <400> but: was <200>“; mit dem Fix grün (`AnthropicApiSupportTest` 42/42, `OpenAiRequestMapperTest` 55/55, `ResponsesApiSupportTest` 70/70, `OpenAiCompatServerHttpTest` 72/72, `OllamaApiSupportTest` 49/49, `AnthropicStreamTranslatorTest` 11/11, `LlamaModelBackendTest` 4/4). `spotless:apply` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `cb0b97f`
+
+
+### F255 – java-llama.cpp: upstreams Abweisung „logprobs is not supported with tools + stream“ ist über den Chat-Mapper unerreichbar
+- **Zeit:** 2026-10-21 14:40 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiRequestMapper.java` (`applyLogprobs`, Klassen-Javadoc), `llama/spotbugs-exclude.xml` (Kommentar der WEM-Unterdrückung), `README.md`
+- **Schwere:** niedrig (ein gestreamter Chat mit Werkzeugen und `logprobs: true` lief mit `n_probs` 20 weiter, wo llama-server 400 antwortet; ob die Wahrscheinlichkeiten in den Werkzeug-Chunks dann stimmen, ist nirgends gemessen)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md „Chat mapper“, dritter Punkt; Brief JR-8), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. F220 hatte upstreams zwei übrige `logprobs`-Regeln übernommen; diese dritte blieb offen, weil der Mapper `stream` nicht liest.
+- **Beschreibung:** upstreams `oaicompat_chat_params_parse` (`server-common.cpp:1457-1461`, b11639) weist bei `json_value(body, "logprobs", false)` mit nicht leeren `tools` (`has_tools`, `:1280`) und `stream` mit `std::invalid_argument("logprobs is not supported with tools + stream")` ab (400). Der Mapper machte `logprobs: true` immer zu `n_probs`; `stream` liest er nicht, weil der Server den Transport wählt. Nur die OpenAI-Chat-Route kann die drei zusammenbringen: Anthropic und Ollama kennen kein `logprobs`, Responses reicht es nicht weiter (F226).
+- **Fix:** `applyLogprobs` wirft bei `logprobs: true`, nicht leerem `tools`-Array und `stream` die `IllegalArgumentException` mit upstreams Text wörtlich, die der Server als 400 beantwortet, bevor ein SSE-Strom beginnt. `stream` wird dafür so gelesen, wie `OpenAiCompatServer.handleChatCompletions` den Streaming-Weg wählt (`asBoolean(false)`), damit Prüfung und Transport übereinstimmen; die Klassen-Javadoc sagt, dass `stream` sonst nicht gemappt wird. Wie upstream nur für das boolesche `logprobs`: eine Zahl (der Schema-Alias von `n_probs`) und ein blockierender oder werkzeugloser Request bleiben erlaubt. Abweichung vom Brief: der Brief nannte „bool true oder Zahl“; upstreams Prüfung greift nur bei `true`, und die stabile Wahl ist, genau sie zu spiegeln.
+- **Tests:** `OpenAiRequestMapperTest#logprobsWithToolsAndStreamIsRefusedLikeUpstream` (Meldung wörtlich), `#logprobsWithToolsOrStreamAloneIsAccepted` (ohne `stream`, ohne `tools`, mit leerem `tools`, mit ganzzahligem `logprobs`; vorher und nachher grün); `OpenAiCompatServerHttpTest#logprobsWithToolsOnAStreamedChatIsA400LikeUpstream` (gestreamt 400 mit der Meldung und nichts generiert, derselbe Body blockierend 200 über `complete`).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot: „OpenAiRequestMapperTest.logprobsWithToolsAndStreamIsRefusedLikeUpstream:310 Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“ und „OpenAiCompatServerHttpTest.logprobsWithToolsOnAStreamedChatIsA400LikeUpstream:992 Expected: is <400> but: was <200>“; mit dem Fix grün (`OpenAiRequestMapperTest` 57/57, `OpenAiCompatServerHttpTest` 73/73, `AnthropicApiSupportTest` 42/42, `ResponsesApiSupportTest` 70/70, `OllamaApiSupportTest` 49/49). `spotless:apply` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `de3c64f`
+
+
+### F256 – java-llama.cpp: Anthropic-`base64`-Bild ohne `media_type` wird immer `image/png`, upstream `image/jpeg`
+- **Zeit:** 2026-10-21 14:46 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java` (`imageUrl`), neu `ImageMediaTypes.java`, `OllamaApiSupport.java` (`toImageDataUrl` nutzt den gemeinsamen Helfer), `README.md`
+- **Schwere:** niedrig (die Data-URL trug für ein JPEG, GIF oder WebP den falschen Typ; llama.cpp dekodiert nach Inhalt, der Typ ist nur eine Angabe – aber falsch und von upstream verschieden)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md „Anthropic Messages“, zweiter Punkt; Brief JR-9), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. F29 führte die Bildblöcke mit dem Rückfall `image/png` ein.
+- **Beschreibung:** `imageUrl` las `source.path("media_type").asText("image/png")`: ohne `media_type` hieß jedes Bild `image/png`. upstream (`server_chat_convert_anthropic_to_oai`, `tools/server/server-chat.cpp:423` und `:483`, b11639) nimmt `json_value(source, "media_type", std::string("image/jpeg"))`, also `image/jpeg`. Die Ollama-Route erkennt den Typ schon an den kodierten Magic-Bytes (`toImageDataUrl`).
+- **Fix:** Ein gemeinsamer Helfer `ImageMediaTypes.ofBase64` (PNG `iVBORw0KGgo`, JPEG `/9j/`, GIF `R0lGOD`, WebP `UklGR`, sonst `null`; die ersten drei Bytes bestimmen die ersten vier Base64-Zeichen, verglichen wird also das kodierte Präfix). Anthropic: ein nicht leeres `media_type` bleibt maßgeblich, ohne es entscheiden die Bytes, unerkannt `image/jpeg` wie upstream. Ollama nutzt denselben Helfer mit seinem bisherigen Rückfall `image/png` (Verhalten unverändert).
+- **Tests:** `AnthropicApiSupportTest#aBase64ImageWithoutMediaTypeIsNamedByItsBytes` (PNG, JPEG, GIF, WebP), `#aBase64ImageOfUnknownBytesWithoutMediaTypeIsJpegLikeUpstream`, `#anExplicitMediaTypeWinsOverTheBytes`; die Ollama-Bildtests bleiben grün.
+- **Rot-Grün-Nachweis:** Ohne den Fix rot: „aBase64ImageWithoutMediaTypeIsNamedByItsBytes:180 image/jpeg Expected: is "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ" but: was "data:image/png;base64,/9j/4AAQSkZJRgABAQ"“ und „aBase64ImageOfUnknownBytesWithoutMediaTypeIsJpegLikeUpstream:191 Expected: is "data:image/jpeg;base64,AAAAAAAA" but: was "data:image/png;base64,AAAAAAAA"“ (die PNG-Zeile war vorher zufällig grün und ist jetzt gepinnt); mit dem Fix grün (`AnthropicApiSupportTest` 45/45, `OllamaApiSupportTest` 49/49, `OpenAiCompatServerHttpTest` 73/73, `LlamaArchitectureTest` 13/13). `spotless:apply` grün.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `80d2d9c`
+
+
+### F257 – java-llama.cpp: Anthropic-Bildquelle `file` (Files-API) und jede andere unbekannte Quelle werden still verworfen
+- **Zeit:** 2026-10-21 14:53 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/AnthropicApiSupport.java` (`imageUrl`, `appendOpenAiMessages`, `toolResultContent`), `README.md`
+- **Schwere:** niedrig bis mittel (ein Bild per `file_id` kam ohne Hinweis nicht beim Modell an; es antwortete, als wäre keines geschickt worden)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md „Anthropic Messages“, erster Punkt; Brief JR-10), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. F29 führte die Bildblöcke ein, der Nachtrag JF-3 pinnte das Verwerfen der `file`-Quelle als upstream-gleich (`#aFileImageSourceIsDroppedLikeUpstream`), J4-5 den Rückfall für eine andere Quelle (`#anUnsupportedImageSourceLeavesTheMessageText`).
+- **Beschreibung:** `imageUrl` lieferte für eine Quelle, die weder `base64` mit String-`data` noch `url` mit String-`url` war, `null`, und der Aufrufer ließ den Block weg; die Nachricht ging als Text weiter, in einem `tool_result` ebenso. upstream (`server_chat_convert_anthropic_to_oai`, `tools/server/server-chat.cpp:417-441` für eine Nachricht, `:478-494` in einem `tool_result`, b11639) wandelt nur `base64` und `url` und verwirft alles andere ebenfalls ohne Fehler. Die Files-API-Quelle `{"type":"file","file_id":…}` kann ein lokaler Server nicht abrufen.
+- **Fix:** Entscheidung (explizit schlägt still, auch gegen upstream): eine Quelle anderen Typs ist eine `IllegalArgumentException` „image source type 'file' is not supported; send base64 or url“ (der Typ steht in der Meldung, ein fehlender als `''`), eine `base64`-Quelle ohne String-`data` „image source of type 'base64' needs a string 'data' (got missing)“, eine `url`-Quelle ohne String-`url` entsprechend; `handleAnthropicMessages` beantwortet sie als 400 `invalid_request_error`, gestreamt wie blockierend vor dem Strom (F223). `imageUrl` gibt nie mehr `null` zurück, beide Aufrufer fügen den Teil ohne Prüfung an. Abweichung vom Brief: die fehlende `data`/`url` einer bekannten Quelle mit erfasst (dieselbe stille Klasse: der Block fiel ebenso weg); `url` wird weiter unterstützt wie bisher.
+- **Tests:** `AnthropicApiSupportTest#aFileImageSourceIsRefused` (ersetzt `#aFileImageSourceIsDroppedLikeUpstream`; Nachricht und `tool_result`), `#anImageSourceOfAnUnknownTypeIsRefused` (ersetzt `#anUnsupportedImageSourceLeavesTheMessageText`; `ftp` und leer), `#anImageSourceWithoutItsDataOrUrlIsRefused`; `OpenAiCompatServerHttpTest#aFileImageSourceOnTheAnthropicRouteIsA400` (gestreamt und blockierend 400 `invalid_request_error` mit der Meldung, nichts generiert). Die alten Pin-Tests prüften das Gegenteil: „content.isTextual() is true“ und „asText() is "what is this?"“ nach dem Verwerfen.
+- **Rot-Grün-Nachweis:** Ohne den Fix rot: „AnthropicApiSupportTest.aFileImageSourceIsRefused:237 {…"source":{"type":"file","file_id":"file_011a"}…} ==> Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“, ebenso `anImageSourceOfAnUnknownTypeIsRefused:249 ftp` und `anImageSourceWithoutItsDataOrUrlIsRefused:266`, und „OpenAiCompatServerHttpTest.aFileImageSourceOnTheAnthropicRouteIsA400:1012 … Expected: is <400> but: was <200>“; mit dem Fix grün (`AnthropicApiSupportTest` 46/46, `OpenAiCompatServerHttpTest` 74/74, `AnthropicStreamTranslatorTest` 11/11, `OllamaApiSupportTest` 49/49, `OpenAiRequestMapperTest` 57/57, `ResponsesApiSupportTest` 70/70). `spotless:apply` grün; die festen Meldungen der ersten Fassung wurden um den gefundenen JSON-Typ ergänzt (die WEM-Regel von SpotBugs, vgl. F227).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `baecd5a`
+
+
+### F258 – java-llama.cpp: die rohen Completion-Pfade weisen upstreams multimodales Prompt-Objekt ab, obwohl ein Projektor geladen ist
+- **Zeit:** 2026-10-21 15:40 UTC
+- **Datei:** `llama/src/main/cpp/jllama_engine.cpp` (`populate_completion_task`, `model_meta`), `llama/src/main/java/net/ladenthin/llama/value/ModelMeta.java` (`getMediaMarker`), `llama/src/test/cpp/tiny_llama_gguf.hpp`
+- **Schwere:** mittel (ein Bild über den rohen Pfad ging mit einem Projektor nie; ein Prompt, der mit dem Bild endet, hätte den Prozess beendet)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md „Ollama routes“; Brief JR-2), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. Gefunden bei der Umsetzung von JR-2: der Brief setzt voraus, dass der rohe Pfad `multimodal_data` versteht; am HEAD tat er es nicht, und der Ollama-Teil (F259) baut darauf auf.
+- **Beschreibung:** upstreams rohes Schema nimmt ein Bild als `"prompt": {"prompt_string": "…  …", "multimodal_data": [""]}` (`tools/server/README.md`, `tokenize_input_subprompt`), tokenisiert im HTTP-Handler mit dem `mtmd_context`. Die Engine erreicht diesen Kontext nicht und rief `tokenize_input_prompts(vocab, nullptr, …)`; das Objekt endete in „Multimodal data provided, but model does not support multimodal requests.“ – auch mit `--mmproj`. Dazu kommt der Marker: upstream zieht ihn je Prozess zufällig (`get_media_marker`, `LLAMA_MEDIA_MARKER` pinnt ihn) und veröffentlicht ihn in `/props`; über `LlamaModel` war er nicht zu erfahren. Und ein Prompt, dessen letztes Stück das Bild ist, lässt dem Slot kein Text-Token für die Logits: `update_slots` bricht dann mit `GGML_ASSERT(batch.size() > 0)` den ganzen Prozess ab (lokal gemessen, JVM-Abbruch mit Exit 134).
+- **Fix:** `populate_completion_task` erkennt das Objekt (`prompt_string` + `multimodal_data`), dekodiert die Einträge (`base64::decode`, ungültige Zeichen sind ein Fehler) und gibt `prompt_string` mit den Bytes über die CLI-Felder der Aufgabe an den Worker, der sie wie die Chat-Pfade mit dem Projektor tokenisiert (`configure_multimodal_task_impl`). Ohne Projektor bleibt upstreams Meldung; ein leeres oder nicht aus Strings bestehendes `multimodal_data` und ein Prompt, der auf den Marker endet, sind `invalid_argument` (400). `model_meta` liefert `media_marker` (wie upstreams `/props`), `ModelMeta#getMediaMarker()` liest ihn. Für einen hermetischen Test schreibt `tiny_llama_gguf.hpp` zusätzlich einen zufälligen llava-artigen Projektor (`write_tiny_mmproj_gguf`, 32×32-Bild, vier Patches).
+- **Tests:** `test_c_api.cpp`: `CApiVision.ModelMetaNamesTheMediaMarker`, `CApiVision.ARawCompletionTakesUpstreamsMultimodalPromptObject` (blockierend, gestreamt, OpenAI-Pfad: `tokens_evaluated` = Text-Tokens + 4 Bild-Embeddings), `CApiVision.AMultimodalPromptWhoseMarkersDoNotMatchItsImagesIsRefused` (kein Marker, zwei Marker für ein Bild, kein Base64, keine Daten, Bild am Ende), `CApi.AMultimodalPromptObjectOnAModelWithoutAProjectorIsTheCallersFault`; `ModelMetaTest#testGetMediaMarker`, `#testGetMediaMarkerEmptyWhenAbsent`; `MultimodalIntegrationTest#aRawCompletionTakesTheMultimodalPromptObject` (JNI, `handleCompletionsOai`).
+- **Rot-Grün-Nachweis:** Ohne den Fix rot: „test_c_api.cpp:775: Failure Value of: j.contains("media_marker") Expected: true“ und, mit dem Marker, „test_c_api.cpp:792: Failure Expected equality of these values: jllama_completions(model_, request.c_str(), &completion.s) Which is: 2 JLLAMA_STATUS_OK Which is: 0 … Multimodal data provided, but model does not support multimodal requests.“; der Java-Test mit einem Prompt, der auf das Bild endete, beendete die Test-JVM („server-context.cpp:4067: GGML_ASSERT(batch.size() > 0) failed“, Exit 134), bevor die Schutzprüfung da war. Mit dem Fix grün: `ctest -R CApi` 49/49, `ModelMetaTest` 20/20, `MultimodalIntegrationTest` mit dem Winzmodell und dem Winz-Projektor 5/6 – rot bleibt nur `#imageBytesAffectTheAnswer`, das echte Farberkennung braucht (Rauschen eines Zufallsmodells; mit SmolVLM in CI).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `595a8be`
+
+
+### F259 – java-llama.cpp: Ollama `/api/generate` mit `raw: true` verwirft `format` und `images` still
+- **Zeit:** 2026-10-21 15:54 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java` (`toOpenAiCompletionRequest`, `toInfillRequest`, neu `formatSchema`, `copyFormatAsJsonSchema`, `hasImages`, `toRawMultimodalPrompt`), `OpenAiCompatServer.java` (`handleOllamaGenerate`), `OpenAiBackend.java` (neu `mediaMarker`), `LlamaModelBackend.java`, `README.md`
+- **Schwere:** niedrig bis mittel (eine rohe Anfrage mit `format` bekam freien Text, ihre Bilder erreichten das Modell nie; beides ohne Hinweis)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md „Ollama routes“, erster Punkt; Brief JR-2), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. F215 hatte den Rest beim Abweisen von `system`/`template`/`context` neben `raw` offen gelassen; F258 macht den rohen Pfad erst bildfähig.
+- **Beschreibung:** `toOpenAiCompletionRequest` übernahm nur `prompt`, Sampling und `stop`. Ollama hält `format` auch roh ein und nimmt Bilder roh über `[img-N]`-Marken im Prompt (sein Runner teilt den Prompt dort); hier kamen weder Grammatik noch Bilder an. Daneben: ein `format`, das weder `"json"` noch ein Schema-Objekt ist, ignorierten alle drei Wege (Chat, Generate mit und ohne `raw`), Ollama weist es ab („invalid format …; expected "json" or a valid JSON Schema object“); der FIM-Weg (`suffix`) verwarf `format` und `images` ebenso.
+- **Fix:** Entscheidung nach dem Brief (Ollama-Verhalten, kein 400 für die Felder selbst): `format` wird auf dem rohen und dem FIM-Weg `json_schema` (`"json"` = `{"type":"object"}`, wie Ollamas JSON-Grammatik und upstreams `json_object`; ein Objekt ist das Schema; `""`/`null` heißen „nicht gesetzt“, wie bei Ollama), ein anderer Wert ist auf allen Wegen 400 „invalid format: ; expected "json" or a JSON Schema object“. Bilder: jede `[img-N]`-Marke wird durch den Medien-Marker des Servers ersetzt (`OpenAiBackend#mediaMarker`, in `LlamaModelBackend` `ModelMeta#getMediaMarker()`, F258), Bild `N` in dieser Reihenfolge nach `multimodal_data`; ein `data:`-URL wird auf seinen Base64-Teil gekürzt. Abweichungen von Ollama, beide laut: eine Marke ohne Bild ist 400 „invalid image index: N“ (bei Ollama ein Fehler des Runners), ein Bild ohne Marke 400 „image N is not placed in the raw prompt; put [img-N] where it goes“ (Ollama lässt es weg). Ein Bild auf dem FIM-Weg ist 400. Abweichung vom Brief: der Brief nahm an, der rohe Pfad nehme `multimodal_data` schon an, und dass Ollamas Bilder ohne Platz im Prompt übergeben werden; beides stimmte nicht (F258, Ollamas `[img-N]`).
+- **Tests:** `OllamaApiSupportTest#aRawGenerationTakesItsFormatAsTheJsonSchema`, `#aFormatThatIsNeitherJsonNorASchemaIsRefused` (roh und Chat, `"yaml"`, `1`, `["json"]`, `true`; `""` gilt als nicht gesetzt), `#aRawGenerationPutsItsImagesWhereTheirTagsAre` (zwei Bilder in vertauschter Reihenfolge, `data:`-URL; ein Prompt mit `[img-0]` ohne Bilder bleibt Text), `#aRawImageWithoutItsTagOrATagWithoutItsImageIsRefused`, `#anInfillGenerationTakesItsFormatAndRefusesImages`; `OpenAiCompatServerHttpTest#ollamaGenerateWithRawCarriesItsFormatAndImages`, `#ollamaGenerateWithAnUnplacedImageOrAnUnknownFormatIsABadRequest` (roh und mit Template, nichts generiert); `OpenAiServerCompletionIntegrationTest#ollamaGenerateRawWithFormatJsonAnswersAJsonObject` (modellgestützt: die Antwort beginnt mit `{`).
+- **Rot-Grün-Nachweis:** Gegen den unveränderten Code (mit den neuen Signaturen als Stummel) rot: „OllamaApiSupportTest.aRawGenerationTakesItsFormatAsTheJsonSchema:265 Expected: is <{"type":"object"}> but: was <>“, „aRawGenerationPutsItsImagesWhereTheirTagsAre:315 Expected: is "a  b  c" but: was """, „aFormatThatIsNeitherJsonNorASchemaIsRefused:287 Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“, ebenso `aRawImageWithoutItsTagOrATagWithoutItsImageIsRefused:329`, `anInfillGenerationTakesItsFormatAndRefusesImages` („Expected: is <{"type":"object"}> but: was <>“), und „OpenAiCompatServerHttpTest.ollamaGenerateWithAnUnplacedImageOrAnUnknownFormatIsABadRequest:520 … Expected: is <400> but: was <200>“, „ollamaGenerateWithRawCarriesItsFormatAndImages:493 Expected: is <{"type":"object"}> but: was <>“. Der modellgestützte Test rot ohne die `json_schema`-Zeile: „OpenAiServerCompletionIntegrationTest.ollamaGenerateRawWithFormatJsonAnswersAJsonObject:321 d"d"dV"d Expected: is  but: was “ (Winzmodell, freier Text). Mit dem Fix grün: `OllamaApiSupportTest` 54/54, `OpenAiCompatServerHttpTest` 76/76, `LlamaModelBackendTest` 4/4, `OpenAiServerCompletionIntegrationTest` 10/12 mit dem Winzmodell (die zwei übersprungenen brauchen FIM-Token).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `e5d35b1`
+- **Nachtrag (Branch-Review 2026-10-20/21):** Ein eigenes `OpenAiBackend`, das `mediaMarker()` nicht überschreibt, bekam auf eine rohe Anfrage mit Bildern 500 statt 501 (Prüfer von J3-routes, Brief JR-23); behoben als [F267](#f267).
+
+
+### F260 – java-llama.cpp: Ollama `/api/generate` ohne `raw` verwirft `template` und `context` still
+- **Zeit:** 2026-10-21 15:58 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OllamaApiSupport.java` (neu `refuseTemplateAndContext`, `TEMPLATE_UNSUPPORTED_MESSAGE`, `CONTEXT_UNSUPPORTED_MESSAGE`; Javadoc von `isRaw`), `OpenAiCompatServer.java` (`handleOllamaGenerate`), `README.md`
+- **Schwere:** niedrig (der Client bekommt eine plausible Antwort, die sein Template bzw. seinen Gesprächskontext nie gesehen hat)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md „Ollama routes“, zweiter Punkt; Brief JR-3), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. F215 weist dieselben Felder neben `raw` ab (Ollamas eigene Regel); ohne `raw` blieb ihr Verwerfen offen.
+- **Beschreibung:** Ohne `raw` rendert Ollama den Prompt mit dem `template` der Anfrage und setzt den Token-`context` einer früheren Antwort fort. Hier rendert das Chat-Template des Modells den Prompt (`toOpenAiChatRequestFromGenerate`), und keine Antwort gibt einen `context` heraus; beide Felder wurden übergangen und die Anfrage als gewöhnlicher Chat-Zug beantwortet.
+- **Fix:** Entscheidung (klarer Fehler statt stiller Abweichung, Form wie F215): ohne `raw` beantwortet `handleOllamaGenerate` ein nicht leeres `template` mit 400 „template is not supported: the model's chat template renders the prompt (got N characters)“ und einen nicht leeren `context` mit 400 „context is not supported: token contexts cannot be replayed; send the conversation as messages to /api/chat (got N tokens)“ (die Länge bzw. der JSON-Typ in Klammern nach der WEM-Regel von SpotBugs, vgl. F227), auch auf dem FIM-Weg (`suffix`); `""`, `[]` und `null` gelten wie bei Ollama als nicht gesetzt. Die Ladeantwort (kein Prompt) bleibt davor. README-Abschnitt der Ollama-Routen nennt die Abweichung.
+- **Tests:** `OpenAiCompatServerHttpTest#ollamaGenerateWithATemplateOrAContextIsRefused` (`template`, `context: [1,2,3]`, `template` neben `suffix` → 400 mit der Meldung, das Backend wird nicht gerufen; leeres `template` und `context: []` → 200 über das Chat-Template).
+- **Rot-Grün-Nachweis:** Gegen den unveränderten Handler (mit den Meldungskonstanten als Stummel) rot: „OpenAiCompatServerHttpTest.ollamaGenerateWithATemplateOrAContextIsRefused:553 {"model":"test-model","prompt":"why?","template":"{{ .Prompt }}","stream":false} Expected: is <400> but: was <200>“; mit dem Fix grün: `OpenAiCompatServerHttpTest` 77/77, `OllamaApiSupportTest` 54/54.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `b99a25b`
+
+
+### F261 – java-llama.cpp: `POST /v1/messages/count_tokens` und die `input_tokens`-Routen antworten 404; upstream zählt dort die Prompt-Token
+- **Zeit:** 2026-10-21 16:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java` (neu `handleCountTokens` mit den drei Routen und ihren Kurzformen, `PATH_MESSAGES_COUNT_TOKENS`, `PATH_RESPONSES_INPUT_TOKENS`, `PATH_CHAT_COMPLETIONS_INPUT_TOKENS`), `OpenAiBackend.java` (neu `countTokens`), `LlamaModelBackend.java` (`countTokens`, `mediaPartType`), `README.md`
+- **Schwere:** niedrig bis mittel (Claude Code fragt `count_tokens` vor langen Anfragen; ein 404 lässt es schätzen bzw. die Funktion ausfallen)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md „Open points from the bug hunt“; Brief JR-19), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. F112 hatte verhindert, dass `/v1/messages/count_tokens` über das Präfix-Matching eine ganze Generierung durch `/v1/messages` lief; seitdem antwortete die Route 404.
+- **Beschreibung:** upstream bedient drei Zählrouten (`tools/server/server.cpp:181-185`, `handle_count_tokens` in `server-context.cpp`, b11639): `/v1/messages/count_tokens` (Anthropic, `{"input_tokens": N}`), `/v1/responses/input_tokens` und `/v1/chat/completions/input_tokens` (dazu `"object": "response.input_tokens"`). Jede übersetzt den Körper wie ihre generierende Route, rendert das Chat-Template (`oaicompat_chat_params_parse`) und tokenisiert den Prompt mit `add_special` und `parse_special`. Hier gab es keine davon.
+- **Fix:** `OpenAiBackend#countTokens(JsonNode)` (Standard: `UnsupportedOperationException` → 501, wie upstream für eine fehlende Fähigkeit), in `LlamaModelBackend` über denselben Weg wie die Generierung: `mapper.toInferenceParameters` → `LlamaModel#applyTemplate` → `handleTokenize(prompt, true, false)`. Die drei Routen (plus `/messages/count_tokens`, `/responses/input_tokens`, `/chat/completions/input_tokens` nach der Kurzform-Regel des Servers) übersetzen mit `AnthropicApiSupport`/`ResponsesApiSupport` wie ihre Elternrouten; eine Übersetzung oder Zählung, die ablehnt, ist 400, eine fehlende Fähigkeit 501 (`not_supported_error` bzw. Anthropics `api_error`), jeweils in der Fehlerform der Route. Abweichung von upstream: eine Anfrage mit Bild, Audio oder Video zählt upstream mit dem Projektor (`process_mtmd_prompt`), den diese Engine nicht erreicht; statt zu wenig zu zählen, antwortet die Route 501 „counting the tokens of a '' content part is not supported; image, audio and video input is counted by the projector“. Abweichung vom Brief: die dritte upstream-Route (`/v1/chat/completions/input_tokens`) mit aufgenommen; `OpenAiCompatServerHttpTest#aSubPathOfARouteIsNotHandledByTheRoute` (F112) prüft jetzt `/v1/messages/batches` und `/v1/chat/completions/other`, da die alte Beispielroute bedient wird.
+- **Tests:** `OpenAiCompatServerHttpTest#theTokenCountRoutesAnswerWithTheCountOfTheTranslatedRequest` (alle sechs Pfade, Antwortform, die übersetzte Anfrage erreicht das Backend, nichts generiert), `#theTokenCountRoutesAnswerRefusalsInTheirRoutesErrorShape` (400 bei abgelehnter Übersetzung, 501 ohne Zählfähigkeit in Anthropic- und OpenAI-Form), `#aSubPathOfARouteIsNotHandledByTheRoute` (angepasst); `LlamaModelBackendTest#aRequestWithAnImageAudioOrVideoPartCountsAsMedia`; `OpenAiCompatServerIntegrationTest#theTokenCountRoutesCountThePromptAGenerationEvaluates` (modellgestützt: die Zählung jeder Route gleicht dem `usage` einer Generierung derselben Anfrage).
+- **Rot-Grün-Nachweis:** Gegen den unveränderten Server (mit `countTokens` als Stummel) rot: „OpenAiCompatServerHttpTest.theTokenCountRoutesAnswerWithTheCountOfTheTranslatedRequest:345 /v1/messages/count_tokens Expected: is <200> but: was <404>“, „theTokenCountRoutesAnswerRefusalsInTheirRoutesErrorShape:386 {"error":{"message":"Not found: /v1/messages/count_tokens",…}} Expected: is <400> but: was <404>“, „OpenAiCompatServerIntegrationTest.theTokenCountRoutesCountThePromptAGenerationEvaluates:92 … Not found: /v1/chat/completions/input_tokens … Expected: is <200> but: was <404>“; mit dem Fix grün: `OpenAiCompatServerHttpTest` 79/79, `LlamaModelBackendTest` 5/5, `OpenAiCompatServerIntegrationTest` 15/15 (Winzmodell als Reasoning-Modell), `AnthropicApiSupportTest` 46/46, `ResponsesApiSupportTest` 70/70.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `2c6e5c5`
+
+
+### F262 – java-llama.cpp: nach begonnenem Stream gilt eine Backend-`IOException` als Verbindungsabbruch — kein Fehler-Event, nur DEBUG
+- **Zeit:** 2026-10-21 17:05 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java` (neu `failStream`; `ResponseStream.writeStrict` merkt sich seine Ausnahme, neu `ResponseStream.isOwnWriteFailure`; die Fehlerzweige von `streamCompletions`, `streamChat`, `streamOllamaChat`, `streamAnthropic`, `streamResponses`)
+- **Schwere:** niedrig (nur über die öffentliche Schnittstelle `OpenAiBackend` erreichbar; `LlamaModelBackend` reicht nur die `IOException` der Senke weiter)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md „Server transport“; Brief JR-6), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. F214 hatte die Hälfte vor dem ersten Chunk behoben (500 statt keiner Antwort) und die Hälfte danach bewusst offen gelassen.
+- **Beschreibung:** Nach dem Start des Streams nahmen die fünf Streaming-Routen jede `IOException` für einen gegangenen Client: `LOG.debug("client disconnected during stream", e)`, kein Fehler-Event, kein `[DONE]`. `OpenAiBackend.stream`/`streamCompletions` dürfen eine `IOException` aber auch für eine gescheiterte Generierung werfen; ein noch verbundener Client sah seinen Stream dann enden, als sei die Antwort vollständig, und im Log stand nichts über WARN.
+- **Fix:** Der Abbruch wird nicht mehr am Typ erkannt, sondern an der Identität: `ResponseStream.writeStrict` merkt sich die Ausnahme, die es wirft, und `isOwnWriteFailure` erkennt sie wieder, auch eingewickelt (Ursachenkette, begrenzt auf 16 Glieder; `LlamaModelBackend.relay` reicht genau diese Ausnahme zurück). Nur sie ist ein DEBUG-Abbruch. Jeder andere Fehler — eine `RuntimeException` oder eine eigene `IOException` des Backends — geht durch den neuen Helfer `failStream`: WARN mit Klassenname und Meldung, vor dem Start 500 mit dem Fehlerkörper der Route, danach das Fehler-Event der Route über `writeQuietly` (SSE-`data:`-Fehlerobjekt für Chat und Completions, Ollama-NDJSON-Zeile `{"error":…}`, Anthropic `event: error`, Responses `response.failed` plus `event: error`). Abweichung vom Brief: der Brief schlug vor, jede `IOException` nach dem Start mit WARN und Event zu behandeln („ein echter Disconnect kostet nichts“); das hätte jeden gewöhnlichen Verbindungsabbruch als WARN geloggt. Die Identitätsprüfung trennt die beiden Fälle genau.
+- **Tests:** `OpenAiCompatServerHttpTest#aBackendIOExceptionAfterTheFirstChunkIsAnErrorEventOnEveryStreamingRoute` (alle fünf Streaming-Routen, `IoFailingAfterFirstChunkBackend`: ein Chunk, dann `IOException("generation failed")`; Status 200, der Chunk, das Fehler-Event der Route mit der Meldung, WARN mit `IOException` per LogCaptor), `ResponseStreamTest#theFailureOfAStrictWriteIsRecognisedAsTheStreamsOwn` (auch als `UncheckedIOException` eingewickelt; eine andere `IOException` nicht), `#noFailureIsTheStreamsOwnBeforeAStrictWriteFailed`.
+- **Rot-Grün-Nachweis:** Gegen den unveränderten Server rot: „OpenAiCompatServerHttpTest.aBackendIOExceptionAfterTheFirstChunkIsAnErrorEventOnEveryStreamingRoute:944 /v1/chat/completions Expected: a string containing "\"type\":\"server_error\"" but: was "data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"he"}}]}“ (der Stream endete nach dem Chunk, ohne Fehler-Event und ohne `[DONE]`); mit dem Fix grün: `OpenAiCompatServerHttpTest` 80/80, `ResponseStreamTest` 7/7.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `359b6d0`
+- **Nachtrag (2026-10-21):** Der erste Stand schrieb die DEBUG-Zeile des Abbruchs als „: the client went away“; `OpenAiServerCompletionIntegrationTest#aClientThatDisconnectsMidStreamIsADisconnectAndFreesTheSlot` (modellgestützt, Winzmodell) wartet auf die feste Zeile „client disconnected during stream“ und lief dadurch nach 30 s rot („the server logged no end of the stream after 30000 ms; debug: [streaming completion: the client went away]“). Die Zeile ist wieder die alte, für alle Routen gleiche; der Test erkennt die WARN-Zeile eines gescheiterten Streams jetzt am Präfix „streaming completion failed“, weil sie seit F262 die Ausnahme nennt (mit dem exakten Listenvergleich hätte seine Negativprüfung nie mehr angeschlagen). Danach grün: `OpenAiServerCompletionIntegrationTest` 12 (2 übersprungen, FIM), `OpenAiCompatServerHttpTest` 80, `ResponseStreamTest` 7.
+
+
+### F263 – java-llama.cpp: ein fehlerhafter Stream-Umschlag beendet den Stream still, als sei die Antwort vollständig
+- **Zeit:** 2026-10-21 17:18 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/json/ChatStreamChunkParser.java` (`feed`, neu `MalformedEnvelopeException`), `llama/src/main/java/net/ladenthin/llama/LlamaModel.java` (neu `feedStreamEnvelope`, genutzt von `streamChatCompletion` und `streamCompletionsOai`)
+- **Schwere:** niedrig (über `LlamaModel` nicht erreichbar, solange die native Seite ihren Vertrag hält; ein Vertragsbruch sähe aus wie eine kurze Antwort)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md „Server transport“, „Decision open: should a malformed stream envelope fail instead of ending the stream?“; Brief JR-7), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen.
+- **Beschreibung:** `feed` nahm einen unparsbaren, leeren oder blanken Umschlag als Ende des Streams (`true`), ein Umschlag ohne `stop` als „weiter“ (`false`), ein `data` ohne Objekt/Array als „nichts“. Die native Seite schreibt aber genau `{"data": , "stop": }` (`wrap_stream_chunk` in `json_helpers.hpp`; `stream_next` in `jllama_engine.cpp` überspringt eine `null`-Nutzlast, ein leerer String wird nie gesendet). Jede andere Form ist ein Transportfehler; das stille Ende ließ ihn wie eine kurze Antwort aussehen (auch über `StreamingChunkAssembler` im langchain4j-Modul, das dem Stream folgt), und ein Umschlag ohne `stop` hätte die Abfrageschleife nie beendet.
+- **Fix:** Vertrag geklärt und festgeschrieben: `feed` liest mit dem strikten Reader (`FAIL_ON_TRAILING_TOKENS`) und verlangt ein JSON-Objekt, `stop` als Boolean und `data` als Objekt oder Array (ein leeres Array liefert keinen Chunk); sonst `ChatStreamChunkParser.MalformedEnvelopeException` (eine `IllegalArgumentException`, das `json`-Paket nutzt keine Exception-Schicht) mit Grund und dem Umschlag als Ausschnitt (höchstens 80 Zeichen wie F232/F233), vor jedem Aufruf der Senke. `LlamaModel.feedStreamEnvelope` macht daraus eine `LlamaException` mit derselben Meldung und der Ursache: aus Sicht des Aufrufers ist das ein Fehler der nativen Schicht, nicht seines Arguments — als `IllegalArgumentException` hätte `OpenAiCompatServer` ihn als 400 `invalid_request_error` beantwortet, jetzt ist er 500. Eine `IllegalArgumentException` der Senke des Aufrufers geht unverändert durch. Der langchain4j-Adapter (`JllamaStreamingChatModel`) reicht die Ausnahme schon über sein `catch (Exception e) → handler.onError(e)` weiter; `StreamingChunkAssembler` brauchte keine Änderung (er bekommt einzelne Chunks und wirft für einen unparsbaren Chunk bereits selbst). Abweichung vom Brief: die zusätzliche Umwandlung in `LlamaException` und der eigene Ausnahmetyp (der Brief nannte nur die `IllegalArgumentException` des Parsers); kein Test in `StreamingChunkAssemblerTest`, weil der Umschlag ihn nie erreicht.
+- **Tests:** `ChatStreamChunkParserTest#feed_anEnvelopeTheNativeSideDoesNotWrite_isRejectedNamingIt` (kein JSON, leer, blank, Array, Boolean, ohne `stop`, `stop` als String, ohne `data`, `data` `null`, `data` Zahl, nachgestellter Text; je Meldung mit dem Umschlag, Senke unberührt), `#feed_aLongBrokenEnvelopeIsNamedByABoundedExcerpt`, `#feed_emptyArrayData_emitsNothing`; die fünf Tests des alten Vertrags (`feed_missingData_emitsNothing`, `feed_stopDefaultsFalse_whenAbsent`, `feed_malformedEnvelope_reportsStop_andEmitsNothing`, `feed_emptyOrBlankEnvelope_reportsStop_andEmitsNothing`, `feed_nullData_emitsNothing`) sind ersetzt. Neu `LlamaModelStreamEnvelopeTest` (wohlgeformt erreicht die Senke; fehlerhaft ist `LlamaException` mit Umschlag und Ursache; die `IllegalArgumentException` der Senke bleibt dieselbe Instanz).
+- **Rot-Grün-Nachweis:** Gegen den unveränderten Parser rot: „ChatStreamChunkParserTest.feed_anEnvelopeTheNativeSideDoesNotWrite_isRejectedNamingIt:78 this is not json ==> Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“ und „feed_aLongBrokenEnvelopeIsNamedByABoundedExcerpt:88 Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.“; mit dem Fix grün: `ChatStreamChunkParserTest` 5/5, `LlamaModelStreamEnvelopeTest` 3/3 (vor `feedStreamEnvelope` nicht kompilierbar), `LlamaArchitectureTest` 13/13. Dass die native Seite den strikten Vertrag erfüllt, zeigen die modellgestützten Stream-Tests mit dem Winzmodell: `OpenAiCompatServerIntegrationTest` 15/15, `OpenAiServerCompletionIntegrationTest` 12 (2 übersprungen, FIM).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `8771717`
+- **Nachtrag (2026-10-21):** `spotbugs:check` am Ende des Durchgangs: die Umwandlung der `IOException` von `readTree` in `feed` (`EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS`) ist mit Begründung unterdrückt wie bei `ChatRequest.parseSchemaOrThrow`; die Meldung nennt die erwartete Form ohne spitze Klammern (`{"data": an object or an array, "stop": a boolean}`), die `POTENTIAL_XML_INJECTION` auslösten. In demselben Commit für F262 das statische Feld von `ResponseStream` vor die Instanzfelder (`IMC_IMMATURE_CLASS_WRONG_FIELD_ORDER`) und für F264 die Wiederherstellung der Build-Info über `restoreBuildInfo` (`MS_EXPOSE_REP`). Danach `spotbugs:check` und `javadoc:javadoc` des Kerns grün.
+- **Nachtrag (PR #554, 2026-10-21):** `LlamaModelStreamEnvelopeTest` rief `LlamaModel.feedStreamEnvelope` und damit den Klasseninitialisierer von `LlamaModel`, der die native Bibliothek lädt; im SonarQube-Job des PR (ein Build ohne Natives) fielen die drei Fälle mit `NoClassDefFoundError` / `UnsatisfiedLinkError`. Die Umsetzung eines fehlerhaften Umschlags in eine `LlamaException` ist reines Java und liegt jetzt in `StreamEnvelope` (paketprivat, von `streamChatCompletion` und `streamCompletionsOai` aufgerufen); der Test heißt `StreamEnvelopeTest` und braucht weder Modell noch Bibliothek. Commit java-llama.cpp `452251b`.
+
+
+### F264 – java-llama.cpp: ein zweites erfolgreiches `LlamaLoader.initialize()` durchläuft die Suche erneut — eine zweite Bibliothekskopie, eine ersetzte Build-Info
+- **Zeit:** 2026-10-21 17:24 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/loader/LlamaLoader.java` (`initialize`, neu `loadOnce`, `reportIgnoredChange`, `resetForTesting`; das Feld `extracted` ist `loaded`), `llama/src/test/java/net/ladenthin/llama/loader/BackendLoadTest.java`, `CLAUDE.md` (RPC-Abschnitt)
+- **Schwere:** niedrig (braucht eine zwischen zwei Aufrufen geänderte `net.ladenthin.llama.lib.path` bzw. einen geänderten Backend-Filter)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md „Java API“; Brief JR-13), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. F245 hatte die Test-Isolation der Build-Info behoben und das Verhalten in der Produktion als Entwurfsfrage offen gelassen.
+- **Beschreibung:** `initialize()` lief bei jedem Aufruf (nur der verschachtelte Aufruf aus `JNI_OnLoad` kehrte früh zurück) die ganze Suche erneut: mit einer inzwischen geänderten `lib.path` oder einem anderen Backend-Filter lud er eine zweite Kopie der Bibliothek (bzw. extrahierte eine zweite Modulkombination in ein eigenes Verzeichnis) und ersetzte die Build-Info, obwohl die JVM die erste Bibliothek nie entladen kann und die native Seite weiter sie benutzt.
+- **Fix:** `initialize()` ist nach dem ersten Erfolg idempotent: `loadOnce` merkt sich den Erfolg (`loaded`, unter dem Initialisierungs-Lock) samt `lib.path` und Backend-Filter zum Ladezeitpunkt; ein weiterer Aufruf lädt nichts und nennt eine inzwischen geänderte Eigenschaft einmal auf stderr („[jllama] the native library is already loaded in this JVM, so net.ladenthin.llama.backend=cuda13,vulkan (cpu at the load) takes effect only in a new JVM“), ein unveränderter Aufruf bleibt still. Ein gescheiterter Versuch wird beim nächsten Aufruf wiederholt. Für die Loader-Tests ein paketprivater Hook `resetForTesting()`, der Erfolgsmerker und Build-Info vergisst (die geladene Bibliothek bleibt geladen) und einen `Runnable` zurückgibt, der den Zustand wiederherstellt; `BackendLoadTest` setzt vor jedem Test zurück und stellt danach wieder her (statt bisher nur die Build-Info). Javadoc von `initialize()` beschreibt den Vertrag. Abweichung vom Brief: statt eines DEBUG-Logs „already loaded“ bei jedem Aufruf meldet der Loader nur eine Änderung, die nicht mehr wirken kann — der Loader schreibt bewusst nur auf stderr (kein SLF4J; ein Router-Worker wertet stdout aus), und `initialize()` wird von jeder Einstiegsklasse aufgerufen, ein Hinweis je Aufruf wäre Rauschen.
+- **Tests:** `BackendLoadTest#aSecondInitializeAfterASuccessfulLoadLoadsNothingAndSaysWhatItIgnores` (Linux x86-64: erster Aufruf mit Filter `cpu` lädt die Dummy-Bibliothek, zweiter mit `cuda13,vulkan` extrahiert nichts und nennt die Änderung auf stderr); `#theDummyLoadsLeaveTheBuildInfoOfThisJvmAsTheyFoundIt` prüft den wiederhergestellten Zustand.
+- **Rot-Grün-Nachweis:** Gegen den unveränderten Loader rot: „BackendLoadTest.aSecondInitializeAfterASuccessfulLoadLoadsNothingAndSaysWhatItIgnores:284 a second start extracted again ==> expected: <[…/jllama-backend-cpu-1a146e4d916-3aa8]> but was: <[…/jllama-backend-cpu-1a146e4d916-3aa8, …/jllama-backend-cpu-1a146e4d916-3aa8-me7c14cbc]>“; mit dem Fix grün: `BackendLoadTest` 10/10, `LlamaLoaderTest` 67/67, `NativeLibraryLoadSmokeTest` 7/7, `LlamaArchitectureTest` 13/13, dazu `BackendLoadTest,LlamaLoggerTest,NativeLibraryLoadSmokeTest` in einer gemeinsamen Fork in beiden Reihenfolgen (`-Dsurefire.runOrder=alphabetical` und `reversealphabetical`) grün — die echte Bibliothek wird nach den Fixture-Läufen weiter geladen bzw. bleibt geladen.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `93be4df`
+
+
+### F265 – java-llama.cpp: das Argument-Präfix vor dem Namen eines gestreamten Werkzeugaufrufs erreicht `onPartialToolCall` nie
+- **Zeit:** 2026-10-21 17:26 UTC
+- **Datei:** `llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/StreamingChunkAssembler.java` (`PartialTool.reportedLength`, `acceptToolCallFragment`, Klassen-Javadoc)
+- **Schwere:** niedrig (bei b11639 nicht erreichbar: llama.cpp sendet Id und Namen mit dem ersten Fragment eines Aufrufs; Härtung gegen eine künftige Parser-Änderung, die `compute_diffs` erlaubt)
+- **Historie:** Offener Punkt aus dem Branch-Review 2026-10-20/21 (TODO.md „Tests“, erster Punkt; Fund J14-1, Brief JR-11), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. Folge des Fixes von F128, der Fragmente ohne Namen nur noch in den Aufruf sammelte, statt am `PartialToolCall` zu scheitern; der Nachtrag zu F128 hatte die Vertragsprüfung `assertPartialsMakeUpTheCompleteCall` für diesen Fall als rot dokumentiert.
+- **Beschreibung:** Argumenttext, der vor dem Namen eines Aufrufs ankam, ging nur in den gesammelten Aufruf; gemeldet wurde danach nur das jeweils aktuelle Fragment. Die Teilstücke eines Aufrufs ergaben daher nicht seine vollständigen Argumente (langchain4js Vertrag), im Beispiel kam `ty":1}` statt `{"city":1}` an.
+- **Fix:** Je Aufruf zählt `reportedLength`, wie viele Zeichen der Argumente schon gemeldet sind. Sobald ein Aufruf einen Namen hat und ungemeldeter Text vorliegt, wird alles ab `reportedLength` als ein `PartialToolCall` gemeldet — auch durch das Fragment, das nur den Namen bringt. So ergeben die Teilstücke stets die vollständigen Argumente, jedes mit endgültiger Id und Namen; das Verhalten für den gewöhnlichen Fall (Kopf-Fragment mit Name, dann Argumente) ist unverändert. Folge für einen Aufruf, dessen Name erst nach dem Beginn des nächsten kommt: sein Präfix wird beim Eintreffen des Namens gemeldet, vor seinem `CompleteToolCall` (Reihenfolge `partial:1, partial:0, complete:0, complete:1`). Klassen-Javadoc angepasst (der Satz, Argumente vor dem Namen „only feed the assembled call“, fällt).
+- **Tests:** `StreamingChunkAssemblerTest#toolCallDeltaBeforeItsName_doesNotFailTheStream` prüft jetzt `assertPartialsMakeUpTheCompleteCall`; `#defersCompletionOfAToolCallThatHasNoNameYetWhenTheNextOneStarts` erwartet das nachgereichte Teilstück von Aufruf 0 (`{}`, Name `first`) vor dessen Abschluss.
+- **Rot-Grün-Nachweis:** Gegen den unveränderten Assembler rot: „StreamingChunkAssemblerTest.toolCallDeltaBeforeItsName_doesNotFailTheStream:197->assertPartialsMakeUpTheCompleteCall:90 Expected: is "{\"city\":1}" but: was "ty\":1}"“; mit dem Fix grün, `StreamingChunkAssemblerTest` 13/13 (die geänderte Erwartung in `defersCompletion…` war mit dem Fix zunächst rot: „Expected: iterable containing ["partial:1", "complete:0", "complete:1"] but: item 1: was "partial:0"“ — genau das nachgereichte Präfix).
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `b528b4e`
+
+
+### F266 – java-llama.cpp: ein mit `--vocab-only` geladenes Modell beendet die JVM bei jeder Server-Operation (SIGSEGV in `llama_vocab::token_bos()`, `getMetrics()` hängt)
+- **Zeit:** 2026-10-21 18:36 UTC
+- **Datei:** `llama/src/main/cpp/jllama_api.cpp` (`run_guarded`, `guarded`, `guarded_tokenizer`; jede `jllama_*`-Funktion auf einem Modell), `llama/src/main/cpp/jllama_engine.{hpp,cpp}` (neu `require_server`; `systemone` nutzt es), `llama/src/main/cpp/jllama_api.h` (Vertrag bei `jllama_model_load`), `llama/src/main/java/net/ladenthin/llama/parameters/ModelParameters.java` (Javadoc von `setVocabOnly()`), `docs/c-api.md`
+- **Schwere:** mittel (ein JVM-Abbruch bzw. ein ewiges Warten aus einem dokumentierten öffentlichen Modus; betrifft JNI, FFM und C gleichermaßen)
+- **Historie:** Folgepunkt des Prüfers aus dem Durchgang 2026-10-21 (Brief JR-25), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. Gefunden beim Brief JR-18 (J5-tests) und als offener Verdacht unter „Offene Verdachte ohne Fund-ID“ eingetragen; der TODO.md-Punkt „Java API“ dazu ist mit diesem Commit entfernt.
+- **Beschreibung:** Ein Modell mit `--vocab-only` (`ModelParameters.setVocabOnly()`) lädt nur das Vokabular, `server_context` bleibt ungeladen und keine Inferenzschleife läuft. Nur `model_meta`, `tokenize`/`tokenize_json`, `detokenize` und `systemone` prüften das. Jeder andere Weg erreichte den leeren Server: `applyTemplate`, alle Abschlusspfade (blocking, gestreamt, OAI, Chat), `infill` und `rerank` stürzten mit `SIGSEGV` in `llama_vocab::token_bos() const` ab (`server_context::get_meta()` über einen Null-Vokabular-Zeiger), `getMetrics`, die Slot-Aktionen und die LoRA-Aufrufe warteten ewig auf ein Ergebnis, das keine Schleife erzeugt. `embed`/`embeddings`/`configure` scheiterten zufällig mit einer irreführenden Meldung, die Stream-Empfänge mit „Task not found“, `task_release` meldete Erfolg.
+- **Fix:** Eine zentrale Prüfung in der C-API, die alle drei Aufrufer (JNI, `llama-ffm`, C) durchlaufen: `guarded()` (Benutzerreferenz + Fehlerabbildung) ruft vor jeder Operation `jllama::engine::require_server(jctx, __func__)`, das bei einem Vocab-only-Modell `std::runtime_error(" is not available in vocab-only mode: the model was loaded with --vocab-only, which loads the tokenizer only (model meta, tokenize, detokenize)")` wirft → `JLLAMA_STATUS_ERROR` bzw. `LlamaException`. Die Ablehnung ist der Normalfall: nur die fünf Tokenizer-Funktionen (`jllama_model_meta`, `jllama_tokenize`, `jllama_tokenize_json`, `jllama_detokenize`, `jllama_detokenize_json`) laufen über `guarded_tokenizer()`, so dass eine künftig hinzugefügte Funktion ohne eigenes Zutun abgelehnt wird. `systemone` nutzt dieselbe Prüfung statt seiner eigenen Meldung. Abweichung vom Brief, begründet: `apply_template` bleibt abgelehnt statt im Vocab-only-Modus zu rendern — die Vorlage läuft über upstreams `oaicompat_chat_params_parse` mit den Chat-Parametern, die der Server beim Laden baut (Vorlagen, Reasoning-Format, Medien-Marker); ein Vocab-only-Laden baut keine, und sie nachzubauen hieße upstreams Laden zu duplizieren. Der Absturz in `token_bos()` kam nicht von einem anders gesetzten Vokabular-Zeiger, sondern vom nie geladenen `server_context`.
+- **Tests:** C++ `test_c_api.cpp`: `CApiVocabOnly.TheTokenizerOperationsAnswer` (Meta, Tokenize, Tokenize mit Stücken, Detokenize, Detokenize-JSON) und der parametrisierte `EveryServerOperation/CApiVocabOnlyRefuses.AnOperationThatNeedsTheServerIsRefusedWithTheVocabOnlyMessage/` mit 22 Instanzen (`apply_template`, `chat_completions`, `chat_completions_stream_request`, `chat_completions_request`, `chat_completions_stream_receive`, `completions`, `completions_oai`, `completions_stream_request`, `completions_oai_stream_request`, `completions_stream_receive`, `task_release`, `infill`, `embed`, `embeddings`, `rerank`, `metrics`, `slot_action`, `lora_adapters`, `set_lora_adapters`, `configure`, `systemone`, `server_start_attached`), je Instanz `JLLAMA_STATUS_ERROR` mit „jllama_ is not available in vocab-only mode“ und „--vocab-only“. Java `VocabOnlyModelTest` (9 Tests, Winzmodell als Draft-Modell): `encode`/`decode`/`getModelMeta` antworten; `applyTemplate`, `complete`, `chatComplete`, `getMetrics`, `eraseSlot`, `setLoraAdapter`, `embed` werfen eine `LlamaException` mit der Meldung; `#theRefusalNamesTheOperation` prüft den Anfang „jllama_apply_template is not available in vocab-only mode“.
+- **Rot-Grün-Nachweis:** Gegen den unveränderten Code jede Instanz in einem eigenen Prozess (`timeout 20 jllama_test --gtest_filter=…`): Exit 139 (`SIGSEGV`) für `apply_template`, `chat_completions`, `chat_completions_stream_request`, `chat_completions_request`, `completions`, `completions_oai`, `completions_stream_request`, `completions_oai_stream_request`, `infill`, `rerank`; Exit 124 (Hänger, nach 20 s abgebrochen) für `metrics`, `slot_action`, `lora_adapters`, `set_lora_adapters`; die übrigen rot an der Meldung, z. B. „Expected: (error.find(std::string("jllama_") + c.name + " is not available in vocab-only mode")) != (std::string::npos), actual: 18446744073709551615 vs 18446744073709551615“ (`embed`, `embeddings`, `configure`, `systemone`, `server_start_attached`, die beiden Stream-Empfänge), `task_release` „Expected equality of these values“ (meldete `JLLAMA_STATUS_OK`). Java gegen die unveränderte Bibliothek: `VocabOnlyModelTest#applyTemplateIsRefused` „The forked VM terminated without properly saying goodbye. VM crash or System.exit called?“ (`hs_err`: „C [libjllama.so+0x7a7874] llama_vocab::token_bos() const+0x4“), die ganze Klasse lief in ein 300-s-Timeout. Mit dem Fix grün: die 23 C++-Fälle (`ctest -R 'CApi|VocabOnly'` 74/74), `VocabOnlyModelTest` 9/9.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `c46e918`
+
+
+### F267 – java-llama.cpp: ein `OpenAiBackend` ohne `mediaMarker()` antwortet auf `/api/generate` mit rohen Bildern 500 statt 501
+- **Zeit:** 2026-10-21 18:41 UTC
+- **Datei:** `llama/src/main/java/net/ladenthin/llama/server/OpenAiCompatServer.java` (`handleOllamaGenerate`), `llama/src/main/java/net/ladenthin/llama/server/OpenAiBackend.java` (Javadoc von `mediaMarker`)
+- **Schwere:** niedrig (nur ein eigenes `OpenAiBackend` — die öffentliche Testnaht — ohne Bildfähigkeit; `LlamaModelBackend` überschreibt die Methode)
+- **Historie:** Folgepunkt des Prüfers aus dem Durchgang 2026-10-21 (Brief JR-23), umgesetzt am 2026-10-21 nach der Entscheidung des Eigentümers, alle offenen Punkte vor 6.0.0 zu schließen. Beobachtet vom Prüfer von J3-routes bei [F259](#f259), das `mediaMarker()` mit einer werfenden Default-Methode eingeführt hatte.
+- **Beschreibung:** Eine rohe Ollama-Anfrage (`raw: true`) mit `images` fragt das Backend nach dem Medien-Marker. Die Default-Methode wirft `UnsupportedOperationException`; die fiel in den generischen Zweig `IOException | RuntimeException` und wurde als Serverfehler 500 beantwortet (mit WARN-Log), obwohl dem Backend nur eine Fähigkeit fehlt — upstream antwortet darauf 501, wie das Repo es für `--embedding`/`--reranking`/FIM und die Token-Zählung (F261) tut.
+- **Fix:** `handleOllamaGenerate` fragt den Marker in einem eigenen `try` und beantwortet eine `UnsupportedOperationException` dort mit 501 und deren Meldung im Ollama-Fehlerformat (`{"error": "raw prompts with images are not supported by this backend"}`). Eng gefasst: eine `UnsupportedOperationException` aus anderen Backend-Aufrufen bleibt ein 500. `mediaMarker()` hat keinen weiteren Aufrufer (`grep`). Der Javadoc der Methode nennt den Vertrag.
+- **Tests:** `OpenAiCompatServerHttpTest#ollamaGenerateWithRawImagesOnABackendWithoutAMediaMarkerIsNotImplemented` (`FakeBackend` behält die Default-Methode; Status 501 und die Meldung).
+- **Rot-Grün-Nachweis:** Gegen den unveränderten Server rot: „OpenAiCompatServerHttpTest.ollamaGenerateWithRawImagesOnABackendWithoutAMediaMarkerIsNotImplemented:591 {"error":"raw prompts with images are not supported by this backend"} Expected: is <501> but: was <500>“; mit dem Fix grün, `OpenAiCompatServerHttpTest` 81/81, `OllamaApiSupportTest` 54/54.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `a4edb90`
+
+
+### F269 – java-llama.cpp: das Klassen-Jar hat keine `Main-Class`, obwohl README, CLAUDE.md und `ServerLauncher` sie versprechen
+- **Zeit:** 2026-10-22 00:40 UTC
+- **Datei:** `llama/pom.xml` (`maven-jar-plugin`, Execution `default-jar`), `.github/smoke-natives-jars.sh`
+- **Schwere:** mittel (jede dokumentierte JBang-Zeile `jbang --deps  net.ladenthin:llama: …` und `java -jar llama-.jar` scheitern mit „no main class deduced“; der Server ist nur über `java -cp … ServerLauncher` erreichbar)
+- **Historie:** Gefunden beim README-Audit am 2026-10-22 (Workflow-Befund L119/L342/L974/L1064/L1134, fünf Stellen). Der `` stand im Assembly-Profil der Fat-Jars (`4f811a2`) und ging mit ihnen; das Klassen-Jar hatte nie einen eigenen. Der `ServerLauncher`-Dispatcher, die README und CLAUDE.md („the classes jar's `Main-Class` is `ServerLauncher`“) beschreiben den Einstiegspunkt, kein CI-Schritt startete ihn über das Manifest (die Smokes nennen die Klasse explizit).
+- **Beschreibung:** `META-INF/MANIFEST.MF` des gebauten `llama-6.0.0-SNAPSHOT.jar` enthielt nur `Manifest-Version`, `Created-By`, `Java-Version`, `Build-Jdk-Spec`. JBang startet eine GAV ausschließlich über deren `Main-Class` (oder `--main`), also liefen alle fünf dokumentierten Zeilen ins Leere.
+- **Fix:** Die Execution `default-jar` des `maven-jar-plugin` in `llama/pom.xml` setzt `net.ladenthin.llama.server.ServerLauncher`; nur sie – die 23 Natives-Executions des `natives`-Profils behalten ihr Manifest (nur `Automatic-Module-Name`), geprüft über `help:effective-pom -P natives` (genau eine `mainClass` neben der des JMH-`exec`). Das Manifest des gebauten Jars: `Main-Class: net.ladenthin.llama.server.ServerLauncher`.
+- **Tests:** `.github/smoke-natives-jars.sh` (Job `package`, lädt die echten Jars) liest `Main-Class` aus dem Klassen-Jar und bricht ab, wenn es nicht `net.ladenthin.llama.server.ServerLauncher` ist – aus dem Jar selbst, nicht aus dem POM, damit ein erneuter Verlust (etwa durch ein Profil, das das Manifest überschreibt) im selben Job auffällt wie damals der Verlust der Fat-Jars.
+- **Rot-Grün-Nachweis:** Manifest vor dem Fix ohne `Main-Class` (gemessen am Jar aus `mvn package`), der neue Smoke-Check liefe damit rot („names '' as Main-Class“); nach dem Fix enthält das Manifest den Eintrag, `bash -n` über das Skript, `check-natives.py` 0 Abweichungen. Kein Unit-Test: ein Manifest-Eintrag ist erst nach `package` beobachtbar, die Prüfung gehört in den Job, der die Jars lädt.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `68ab75d`
+- **Nachtrag (2026-10-22, Lauf 39763815465):** Der Smoke-Check selbst hatte mit diesem Commit seine Classpath-Zeile verloren (`path="$classes:…:$DEPS"` wich der `Main-Class`-Prüfung), und `bash -n` sieht eine unbelegte Variable nicht: der erste `package`-Job, der den Schritt seit dem Sign-Job-Fix erreichte, starb mit „line 84: path: unbound variable“. Zeile wiederhergestellt, mit einem Stub-`java` nachgewiesen (committetes Skript rot mit derselben Meldung, repariertes grün, der Stub bekommt Klassen-Jar, alle Natives-Jars und die Abhängigkeiten auf Classpath und Modulpfad). Commit java-llama.cpp `9c835e8`.
+
+
+### F270 – java-llama.cpp: `LlamaModelTest.testGenerateGrammar` / `testCompleteGrammar` zählen die erzeugten Token über eine Re-Tokenisierung des Textes
+- **Zeit:** 2026-10-22 01:10 UTC
+- **Datei:** `llama/src/test/java/net/ladenthin/llama/LlamaModelTest.java` (`testGenerateGrammar`, `testCompleteGrammar`)
+- **Schwere:** niedrig (Testfehler; der Windows-x86-64-Job des Laufs 39692853697 rot, `testGenerateGrammar:201 expected:  but was: `)
+- **Historie:** Die Tests prüfen die Grammatik `root ::= ("a" | "b")+` mit `n_predict` 10 und ohne festen Seed; die zweite Zusicherung las die Zahl der erzeugten Token als `model.encode(output).length <= nPredict + 1`. Die Grammatik lässt mehrbuchstabige Stücke zu (`ab`, `aab`, …), und der Encoder zerlegt deren Verkettung anders, als sie gesampelt wurde: ein Text aus höchstens 10 Stücken re-tokenisierte auf dem Windows-Läufer zu mehr als 11 Token. Welche Stücke fallen, ist Zufall (kein Seed, Temperatur-Sampling) und hängt am CPU-Modul des Läufers; die Zusicherung war damit eine Lotterie, keine Messung. Die erste Zusicherung (`[ab]+`) hielt.
+- **Fix:** Der Stream-Test zählt die Chunks mit Text (jeder trägt mindestens ein gesampeltes Token, also eine untere Schranke, die `n_predict` nie überschreitet), der Blocking-Test liest `completeWithStats(...).getTimings().getPredictedN()`, die Zählung des Servers. Beide Zusicherungen nennen den Text und die Zahl in ihrer Meldung.
+- **Tests:** dieselben zwei Tests; modellgebunden (CodeLlama 7B Q2_K), lokal nicht ausführbar (kein Modell im Sandkasten).
+- **Rot-Grün-Nachweis:** rot im CI-Lauf 39692853697 (Windows 2025 x86-64, clang), grün erwartet im Lauf auf dem nächsten Head; `test-compile` und Spotless lokal.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `9daae5b`
+
+
+### F271 – java-llama.cpp: `jllama_rpc_server_stop()` beendet auch den Serve, der startet, während der Stop auf den gemeinten wartet
+- **Zeit:** 2026-10-22 01:20 UTC
+- **Datei:** `llama/src/main/cpp/jllama_api.cpp` (`rpc_serve_state`, `rpc_serving_scope`, `jllama_rpc_server_stop`), `llama/src/main/cpp/jllama_api.h`, `llama/src/main/cpp/rpc_support.hpp` (Test-Naht `set_stop_hook_for_testing`), `llama-ffm/.../FfmRpcServer.java` (Javadoc)
+- **Schwere:** niedrig bis mittel (nur C- und FFM-Aufrufer, die aus einem anderen Thread neu starten; das Java-`RpcServer` hält seinen Slot bis `close()` zurück ist)
+- **Historie:** Gefunden vom Opus-Review des PR #554 am 2026-10-22. Seit F241 wiederholt der Stop ggmls `stop_server` alle 50 ms, bis `serving == 0`, und `stop_requested` wurde erst bei `serving == 0` zurückgesetzt. Nimmt ein neuer `jllama_rpc_server_serve()` den Mutex, bevor der wartende Stop `serving == 0` sieht, zählt der Neue mit: der Stop wartet auf ihn (bis zum 30-s-Timeout), er liest `stop_requested` noch als gesetzt und kehrt zurück, ohne je zu binden – oder, falls er gebunden hat, trifft ihn der wiederholte ggml-Stop. Header und FFM-Javadoc versprachen das Gegenteil („does not carry over to the next one“).
+- **Fix:** Jeder Serve-Aufruf bekommt eine Id (`active`, älteste zuerst); der Stop merkt sich die jüngste als Ziel (`stop_up_to`), ein Serve fragt `id <= stop_up_to`, und der Stop wartet nur, solange ein Serve mit Id bis zum Ziel aktiv ist. ggmls Stop wird unter dem Mutex ausgelöst, den die Registrierung eines neuen Serve zuerst nimmt; der Neue bindet danach, und ggml löscht sein Flag beim Binden. Header-Kommentar, FFM-Javadoc, CLAUDE.md und CHANGELOG angepasst.
+- **Tests:** `test_c_api.cpp` `CApiRpc.AStopDoesNotReachTheServeStartedAfterIt`: der erste Serve hängt in der Serve-Naht, der Stop läuft auf dem Testthread; die neue Stop-Naht (nach dem Vermerk des Ziels) startet den zweiten Serve, hält ihn in der Serve-Naht und gibt den ersten frei. Der Stop muss mit OK zurückkehren, sobald der erste beendet ist; der zweite muss nach der Freigabe binden und lauschen, ein zweiter Stop beendet ihn.
+- **Rot-Grün-Nachweis:** mit der alten Implementierung (nur die Naht eingezogen) rot: „the RPC server did not stop within 30 s of the stop request“ und „the stop of the first serve ended the second one“ (30 002 ms); mit dem Fix grün (12 ms), dreimal einzeln und zusammen mit den vier anderen `CApiRpc`-Fällen; ganze C++-Suite 686/686; clang-format 23.1.3.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `c59569a`
+
+
+### F272 – java-llama.cpp: `ChatScenarioTest`, `ChatAdvancedTest` und `MemoryManagementTest` vergleichen zwei Generierungen über einen Prompt-Cache-Treffer hinweg
+- **Zeit:** 2026-10-22 09:31 UTC
+- **Datei:** `llama/src/test/java/net/ladenthin/llama/ChatScenarioTest.java` (`testChatCompleteTextMatchesChatCompleteContent`), `ChatAdvancedTest.java` (`testCachePromptConsistentOutput`), `MemoryManagementTest.java` (`testPromptCacheStableAcrossMultipleCalls`); `CLAUDE.md` (Regel unter „Java tests“)
+- **Schwere:** niedrig (Testfehler; der MSVC-Windows-Job des Laufs 39702748614 rot: `ChatScenarioTest.testChatCompleteTextMatchesChatCompleteContent:163 … expected: <4.<|im_end|>\n> but was: <2 plus 2 is 4.<|im>`)
+- **Historie:** Die drei Tests stellen zwei bzw. drei identische Greedy-Anfragen (Temperatur 0, fester Seed) und setzen die Texte gleich. Zwischen der ersten und der zweiten Anfrage liegt ein Prompt-Cache-Treffer: mit `cache_prompt` (Vorgabe) übernimmt die zweite alle Prompt-Token bis auf das letzte aus dem Slot und wertet nur dieses eine neu aus (upstream `server-context.cpp`, `[TAG_PROMPT_LOGITS]`: „need to evaluate at least 1 token for each active slot“, „n_past was set to 158“). Die Logits dieser Ein-Token-Auswertung sind nicht bitgleich mit denen der gebündelten Prompt-Auswertung der ersten Anfrage (andere Batch-Form, andere Kernel und Summationsreihenfolge), und bei einem knappen Gleichstand kippt der Greedy-Argmax: CodeLlama 7B Q2_K antwortete auf „What is 2 plus 2?“ einmal „2 plus 2 is 4.<|im“ und einmal „4.<|im_end|>“. Gemessen am Zufallsmodell `tiny2k.gguf` (2 Schichten, 4000 Zufallsprompts, je drei Anfragen, `n_probs` 3): erste gegen zweite Anfrage – Logprob des ersten erzeugten Tokens verschieden in 3999 von 4000 Paaren (höchstens 5,6e-3), Text verschieden in 4 (etwa `'1Q]RNQ'` gegen `'Q]RNQ]'` bei `cache_n` 112, `prompt_n` 1); ohne Cache (150 Paare) 0 Unterschiede, bitgleich. Zweite gegen dritte Anfrage (beide über demselben Cache-Präfix, je ein Token neu): 0 Text- und 0 Logprob-Unterschiede in 4000 Paaren; in allen 8000 Cache-Anfragen galt `cache_n == prompt_tokens - 1` und `prompt_n == 1`. Der Vergleich erste gegen zweite war damit eine Lotterie mit kleiner Verlustwahrscheinlichkeit, kein Nachweis – dieselbe Klasse wie F270.
+- **Fix:** `ChatScenarioTest`: beide Anfragen mit `withCachePrompt(false)`; die zweite (`chatComplete`, die Rohantwort) belegt die Vorbedingung über `timings` (`cache_n` 0, `prompt_n` gleich `usage.prompt_tokens`), sodass ein später entferntes Flag den Test deterministisch rot macht statt ihn zur Lotterie zurückzubauen. `ChatAdvancedTest` und `MemoryManagementTest`: drei Anfragen über `completeWithStats`; die zweite und die dritte müssen den Cache nutzen (`cache_n == prompt_tokens - 1`, `prompt_n == 1`) und einander gleichen; die erste wird nur auf nicht-leer geprüft und absichtlich nicht mit ihnen verglichen (die Javadoc nennt den Grund). `CLAUDE.md` hält die Regel fest. Kein Produktionscode: das Verhalten ist upstreams und kein Fehler, der Server muss für die Logits mindestens ein Token auswerten.
+- **Tests:** dieselben drei Tests. Messprogramm im Sandkasten (nicht eingecheckt): je Prompt zwei bzw. drei `completeWithStats` mit `n_probs`, Vergleich von Text, erstem Logprob und `timings`.
+- **Rot-Grün-Nachweis:** rot im CI-Lauf 39702748614 (Windows 2025 x86-64, MSVC) mit dem alten Vergleich; lokal mit dem Zufallsmodell der alte Vergleich (erste gegen zweite Anfrage) rot in 4 von 4000 Prompts, der neue (zweite gegen dritte) grün in 4000 von 4000; die drei umgebauten Tests grün mit `tiny2k.gguf` (je 1 Test, 0 übersprungen); Spotless. Modellgebunden (CodeLlama 7B Q2_K): grün erwartet im nächsten Lauf.
+- **Nachtrag (Sweep 2026-10-22):** Ein Lesedurchgang über alle Testbäume (Java, C++, FFM, langchain4j, Kotlin, Smokes) fand acht weitere Vergleiche derselben Form, alle umgebaut -- Form A: beide Anfragen `cache_prompt=false`, Vorbedingung über `timings` belegt; Form B: zweite gegen dritte Cache-Anfrage, Wiederverwendung über `timings` belegt: `MemoryManagementTest#testPromptCacheGivesDeterministicOutput` (B; Helfer `assertCachedPrefixReused`, auch in `#testPromptCacheStableAcrossMultipleCalls`), `#testPromptCacheCompleteMissAfterWarmup` (das frische Modell bekommt denselben Warm-up, beide Antworten mit `cache_n <= 1` und gleichem `cache_n` / `prompt_n`; der Warm-up selbst ohne Cache, damit ein Präfix eines früheren Tests im geteilten Slot nicht hineinspielt), `LlamaModelTest#testCompleteAsync` (A), `ChatScenarioTest#testChatCompleteWithStopString` (A; aus der Längenprüfung wurde `unContent.startsWith(stContent)`, Helfer `assertWholePromptEvaluated` auch im ersten Test der Klasse), C++ `LoraCache.TheAdapterChangesTheAnswerAndTheSlotReusesItsCache` (B: eine dritte Anfrage mit gleichem `cache_n` / `prompt_n`), `LoraCacheFreshSlot.AStateRestoredIntoANeverUsedSlotIsKept` (der wiederhergestellte Slot 1 gegen Slot 0 nach dem Speichern, beide mit je einem neu ausgewerteten Token), `CApiReasoningBudget.ARawCompletionWithMessagesKeepsTheReasoningBudgetOfTheChatTemplate` (A; Chat-, Roh- und Stream-Anfrage ohne Cache, Helfer `without_prompt_cache` / `expect_whole_prompt_evaluated`) und `CApi.ARawCompletionWithMessagesAndATemplateWithoutThinkingTagsIgnoresTheBudget` (A; hielt bisher nur durch die gtest-Reihenfolge, der Test davor ließ denselben Prompt im Slot). Als korrekt gelesen und unverändert: die Vergleiche `after_change` gegen `fresh_answer()` in `test_lora_cache.cpp` (beide `cache_n` 0, gleiche Batch-Form), Stream gegen Blocking in `test_c_api.cpp` (zwei Cache-Treffer über demselben Präfix), Fälle, in denen Bias oder Grammatik jedes Token erzwingen, Prompt-Token-Zählungen, zustandslose Embedding- und Rerank-Vergleiche mit Toleranz, `assertNotEquals`-Fälle. Grün: ctest 686/686 (b11814-Build mit den geänderten Tests), die sieben Java-Tests mit `tiny2k.gguf` (0 übersprungen); Spotless, clang-format.
+- **Status:** gefixt
+- **Commit:** java-llama.cpp `85ebbfd`
+- **Nachtrag-Commit:** java-llama.cpp `f64a42e`
+
+## Offene Verdachte ohne Fund-ID
+
+- *(geschlossen 2026-10-19, ohne Fund-ID, Entscheidung des Nutzers: bewusst nicht ändern)* Continue entscheidet die FIM-Fähigkeit eines Ollama-Modells an `{{ .Suffix }}` im `template` von `/api/show`; wir liefern `""`. Ein Platzhalter-Template wäre erfunden (der Server kennt das Prompt-Format des Modells nicht) und für die meisten Modelle falsch. Continue nutzt dann eigene Vorlagen; für die FIM-Token des Modells gibt es `POST /infill` (Continues `llama.cpp`-Provider). README-Hinweis ergänzt.
+- *(erledigt 2026-10-21 als [F266](#f266); gefunden 2026-10-21 beim Brief JR-18)* **Ein mit `--vocab-only` geladenes Modell (`ModelParameters.setVocabOnly()`) beendet die JVM bei jeder Server-Operation.** Gemessen mit dem Winzmodell und mit upstreams `models/ggml-vocab-llama-spm.gguf`: `applyTemplate(...)` und `complete(...)` stürzen mit `SIGSEGV` in `llama_vocab::token_bos() const` ab (`hs_err`: `C [libjllama.so+0x7a7874]`, aus `LlamaModel.applyTemplate`), `getMetrics()` hängt (nach 60 s abgebrochen). `jllama_engine.cpp` prüft `jctx->vocab_only` nur in `model_meta`, `tokenize`/`tokenize_json`, `detokenize` und `systemone`; alle anderen Wege (`parse_oai_chat_params`, die Abschlusspfade, `metrics`, Slots, LoRA …) erreichen den nie geladenen `server_context`. Erwartet wäre eine `LlamaException` („… is not available in vocab-only mode“, wie `systemone` sie wirft) an jeder Engine-Funktion, die einen Server braucht, mit einem Test je Weg.
diff --git a/BUGHUNT-MEASUREMENTS.md b/BUGHUNT-MEASUREMENTS.md
new file mode 100644
index 000000000..051f220b2
--- /dev/null
+++ b/BUGHUNT-MEASUREMENTS.md
@@ -0,0 +1,1097 @@
+
+
+# Messauftrag zu F67 und F179 … F186 — java-llama.cpp
+
+Die Fixes vom 2026-10-19/20 sind alle modellfrei rot/grün belegt: C++ über ein Zufallsmodell, Java über
+Fake-Backends oder ein lokal erzeugtes Zufalls-GGUF. Was ein Zufallsmodell nicht zeigen kann — echte
+Texte, echte Stoppwörter, echte Wahrscheinlichkeiten, echte Adapter, Windows-Dateinamen — sammelt dieser
+Auftrag. Hintergrund in `BUGHUNT-FOUND.md` (die Fund-IDs) und `CHANGELOG.md` („Fixed“).
+
+**Regeln für den ausführenden Agenten**
+
+- **Nur messen und berichten.** Keine Produktionsänderung, keine Fixes. Wegwerf-Proben (Testklassen,
+  kleine `main`-Programme) nach dem Lauf entfernen; committet wird nur diese Datei mit den Ergebnissen.
+- Arbeitsstand: Branch `ccr-90fc5f6a-vs4s0f` (mindestens `573619a`). Pin: llama.cpp b11639.
+- Dateien aus `.github/shared-files.sha256` nicht verändern.
+- Jeder Punkt wird beantwortet, auch wenn er nicht ausführbar war (mit Grund). Zu jedem Ergebnis die
+  Umgebung (CPU, Kerne, RAM, GPU, OS, JDK) und die wörtlichen Ausgaben, die das Ergebnis tragen.
+- Ergebnis unter **Ergebnis:**, darunter **Folgerung:** (bestätigt der Lauf den Fix, oder was weicht ab).
+- Ein Test, der hier rot wird, ist ein Befund: Ausgabe vollständig eintragen, nicht „reparieren“.
+
+**Eine Ausnahme von der ersten Regel, ausdrücklich beauftragt:** die Befunde dieses Auftrags wurden
+nach Abschluss der Messungen behoben, jeder mit einem Wächter und jeder end-to-end belegt — **B1**
+(M6, `--lora-scaled` rendert das falsche Werteformat) samt dem dabei gefundenen **B4** (die vier
+`addX`-Methoden ersetzten statt anzuhängen), **B2** (M5, `/v1/models` und die Antworten benennen das
+Modell verschieden) und **B3** (M8, der Testharnisch schreibt unescapte Steuerzeichen in sein JSON).
+Die Messabschnitte selbst stehen unverändert so da, wie sie gemessen wurden; was danach geändert
+wurde, steht jeweils in einem eigenen Unterabschnitt „ist behoben". Nicht angefasst sind die
+Nebenbeobachtungen aus M7 und der eingebettete Weg aus B2 — bei beiden ist begründet, warum.
+
+Die vier Befunde stehen mit ihren globalen Fund-IDs in [`BUGHUNT-FOUND.md`](BUGHUNT-FOUND.md): **F187** (B1), **F188** (B4), **F189** (B2) und **F190** (B3).
+
+**Gemeinsame Vorbereitung**
+
+```bash
+# JDK: Temurin 21 (nicht Oracle 21, siehe CLAUDE.md); für M10 zusätzlich ein JDK 22+ in ~/.m2/toolchains.xml
+mvn -B -q -f llama/pom.xml compile
+cd llama && cmake -B build -DBUILD_TESTING=ON && cmake --build build --config Release -j"$(nproc)" && cd ..
+ctest --test-dir llama/build --output-on-failure        # erwartet: 643/643
+# Modelle aus .github/models.csv nach models/ (die Tests finden sie dort):
+mkdir -p models
+while IFS=, read -r f url; do case "$f" in \#*|"") continue;; esac
+  [ -f "models/$f" ] || curl -L --fail -o "models/$f" "$url"; done < .github/models.csv
+# Auf einem Rechner ohne GPU alle Läufe mit -Dnet.ladenthin.llama.test.ngl=0
+```
+
+Für die kleinen Messprogramme unten genügt eine Wegwerf-Testklasse unter
+`llama/src/test/java/net/ladenthin/llama/` (dann gilt der Test-Klassenpfad mit `src/main/natives/`),
+gestartet mit `mvn -B -q -f llama/pom.xml test -Dtest=`. Pfade über `TestConstants`
+(`MODEL_PATH`, `DRAFT_MODEL_PATH`, `REASONING_MODEL_PATH`, `DEFAULT_TOOL_MODEL_PATH`).
+
+---
+
+## Umgebung aller Läufe (einmal, gilt für M1 … M10)
+
+| | |
+|---|---|
+| CPU | AMD Ryzen 7 5800H with Radeon Graphics, 8 Kerne / 16 Threads |
+| RAM | 63,3 GiB |
+| GPU | NVIDIA GeForce RTX 3070 Laptop (Treiber 32.0.15.8183) + AMD Radeon (iGPU) — **ungenutzt**, siehe unten |
+| OS | Microsoft Windows 11 Pro 10.0.26300 (Build 26300.9711) |
+| JDK (Maven, Kern) | Temurin 21.0.12.1+1 — **nicht** das daneben installierte Oracle 21.0.9, das `mvn compile` laut CLAUDE.md in Error Prone abbricht |
+| JDK (`llama-ffm`) | Temurin 25.0.4.1+1, über einen neu angelegten `~/.m2/toolchains.xml` |
+| Maven | 3.9.11 |
+| C++-Toolchain | MSVC 19.44.35229.0 (`cl.exe`), Ninja Multi-Config, CMake 3.31.6-msvc6 aus VS 2022 Community |
+| Build | statisch mit `-DBUILD_TESTING=ON` (`.github\build.bat`), **ohne** sccache (`USE_CACHE=false`) |
+
+**Der Build ist CPU-only.** Es wurde kein GPU-Modul gebaut, das Natives-Verzeichnis trägt genau
+`Windows/x86_64/cpu/`, und `JNI_OnLoad` meldet in jedem Lauf
+`[jllama] native backend 'cpu' loaded from … with no GPU module`. Alle Läufe daher mit
+`-Dnet.ladenthin.llama.test.ngl=0`; die RTX 3070 ist an keiner Messung beteiligt.
+
+`llama/src/main/natives/net/ladenthin/llama/Windows/x86_64/cpu/jllama-build.txt` des gemessenen Builds:
+
+```
+llama.cpp=b11639
+llama.cpp.commit=a0e6dc7384edb8f5d927155e1d26c10b4a310e6d
+backend=cpu
+kind=library
+classifier=cpu-windows-x86-64
+jllama.version=6.0.0-SNAPSHOT
+jllama.commit=86d0f8a8746feb4accc998c67c80e44ffd63874f
+patches=0001,0002,0003,0006,0007,0008,0014,0015,0016,0017,0018,0019,0020,0021
+patches.sha256=0176a9b12e147b97027b393ca107f559bc9c997f6076b5a94390957bf10106b8
+compiler=MSVC 19.44.35229.0
+toolkit=none
+signed=no
+```
+
+Arbeitsstand also `86d0f8a8` auf `ccr-90fc5f6a-vs4s0f` (die verlangte Mindesthöhe `573619a` ist
+enthalten), Pin b11639, alle 14 Patches angewandt.
+
+**Modelle.** Die elf Zeilen aus `.github/models.csv` lagen bereits vollständig in `models/`; nichts
+wurde nachgeladen. Für M6 kamen `stories15M_MOE-F16.gguf` (73 466 432 B) und
+`moe_shakespeare15M.gguf` (16 364 896 B) von `ggml-org/stories15M_MOE` hinzu, beide mit GGUF-Magic
+`47475546`.
+
+**Vorbereitung.** `mvn -f llama/pom.xml compile` grün; der Build lief 549/549 Schritte durch; danach
+
+```
+100% tests passed, 0 tests failed out of 643
+Total Test time (real) = 150.90 sec
+```
+
+also die vom Auftrag erwarteten **643/643**.
+
+Ein Hinweis zum Ausgangszustand: die vorhandenen Natives waren `llama.cpp=b11538` vom 11.10. und
+damit älter als Pin und Patches `0020`/`0021`. `llama/build` und `llama/src/main/natives` wurden
+deshalb gelöscht und alles frisch gebaut.
+
+---
+
+## M1 Die neuen und geänderten modellgestützten Tests
+
+**Ziel.** Die Tests, die nur in CI mit Modellen laufen, einmal lokal mit dem echten Modellsatz grün sehen.
+
+```bash
+mvn -B -f llama/pom.xml test -Dnet.ladenthin.llama.test.ngl=0 -Dtest='ErrorHandlingTest,OpenAiServerCompletionIntegrationTest,OpenAiServerRerankIntegrationTest,OpenAiCompatServerIntegrationTest,LlamaTrainerTest,LlamaTrainerIntegrationTest'
+```
+
+Erwartet: alle grün, keiner übersprungen. Im Einzelnen:
+`ErrorHandlingTest#testNPredictMinusTwoPerRequestIsAnInvalidRequest` (F69: `n_predict: -2` pro Anfrage
+ist `InvalidRequestException`); `OpenAiServerCompletionIntegrationTest#aStreamedCompletionIsUpstreamsStream`
+(F186), `#ollamaGenerateRawRoundTrip` und `#ollamaGenerateNonStreamingRoundTrip` (F179);
+`OpenAiServerRerankIntegrationTest` (F177).
+
+**Ergebnis:** Alle sechs Klassen grün, **keine übersprungen**:
+
+```
+[INFO] Running net.ladenthin.llama.ErrorHandlingTest
+[INFO] Tests run: 17, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.544 s
+[INFO] Running net.ladenthin.llama.LlamaTrainerIntegrationTest
+[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.793 s
+[INFO] Running net.ladenthin.llama.LlamaTrainerTest
+[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.021 s
+[INFO] Running net.ladenthin.llama.server.OpenAiCompatServerIntegrationTest
+[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.583 s
+[INFO] Running net.ladenthin.llama.server.OpenAiServerCompletionIntegrationTest
+[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 12.78 s
+[INFO] Running net.ladenthin.llama.server.OpenAiServerRerankIntegrationTest
+[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.303 s
+[INFO] Tests run: 40, Failures: 0, Errors: 0, Skipped: 0
+[INFO] BUILD SUCCESS
+```
+
+Weil eine Klasse mit einer `@BeforeAll`-Annahme als `tests="0"` durchläuft und damit unsichtbar
+bliebe (CLAUDE.md, „CI model policy"), sind die namentlich genannten Methoden zusätzlich einzeln aus
+den surefire-XML belegt — jede mit einer eigenen ``-Zeile und einer Laufzeit > 0:
+
+```
+name="testNPredictMinusTwoPerRequestIsAnInvalidRequest" classname="net.ladenthin.llama.ErrorHandlingTest" time="0.06"
+name="aStreamedCompletionIsUpstreamsStream" classname="…OpenAiServerCompletionIntegrationTest" time="0.438"
+name="ollamaGenerateRawRoundTrip"           classname="…OpenAiServerCompletionIntegrationTest" time="2.106"
+name="ollamaGenerateNonStreamingRoundTrip"  classname="…OpenAiServerCompletionIntegrationTest" time="2.881"
+testcase name="rerankReturnsScoredResultsCappedByTopN"                       (OpenAiServerRerankIntegrationTest)
+testcase name="aFourByteCharacterInAPathComesBackIntactInTheErrorMessage"    (LlamaTrainerTest)
+testcase name="aFourByteCharacterInTheTrainingTextReachesTheTrainer"         (LlamaTrainerTest)
+testcase name="finetuneWritesAnOutputModel"                                  (LlamaTrainerIntegrationTest)
+```
+
+**Folgerung:** Der Lauf bestätigt die Erwartung. F69, F186, F179 und F177 sind mit dem echten
+Modellsatz auf Windows belegt, und `LlamaTrainerTest` — in CI nur auf Linux — läuft auch hier grün,
+was die erste Hälfte von M7 bereits mit abdeckt.
+
+---
+
+## M2 F186: das gestreamte `/v1/completions` mit dem echten Modell
+
+**Ziel.** Der Integrationstest prüft die Form; hier zusätzlich der Inhalt. Mit `OpenAiCompatServer` über
+`MODEL_PATH` (CodeLlama) dieselbe Anfrage einmal blockierend und einmal gestreamt, Greedy:
+
+```json
+{"prompt": "def fibonacci(n):", "max_tokens": 24, "temperature": 0, "top_k": 1, "logprobs": 3}
+```
+
+Aufzeichnen: (a) der zusammengesetzte Text aller gestreamten `choices[0].text` gegen `choices[0].text`
+der blockierenden Antwort; (b) `usage` des letzten Chunks gegen `usage` der blockierenden Antwort;
+(c) die ersten drei `logprobs.content`-Einträge beider Wege; (d) dasselbe mit `"prompt": []` (Ids über `LlamaModel.encode`); (e) `"prompt": ["a", "b"]` gestreamt und
+blockierend (erwartet: beide `400`, Meldung nennt „2 prompts“, F184).
+
+**Hypothese.** (a) gleich, (b) gleiche Zahlen, (c) gleiche Tokens und Werte, (d) wie (a), (e) zweimal 400.
+
+**Ergebnis:** Der erste Durchgang bestätigte (a), (d) und (e) sofort, zeigte aber in (b) und (c)
+Abweichungen — die sich als **Prompt-Cache** erklärten: das Paar läuft blockierend zuerst, die
+gestreamte Anfrage trifft denselben Slot warm. Deshalb lief dasselbe Paar ein zweites Mal mit
+`"cache_prompt": false`. Beide Durchgänge unten.
+
+**(a) Text, mit Prompt-Cache** — identisch, blockierend wie gestreamt (25 Chunks):
+
+```
+  (a) blocking text = [
+  if n == 0:
+    return 0
+  elif n == 1:
+    return]
+  (a) streamed text = [… byte-identisch …]
+  (a) EQUAL=true
+```
+
+**(b) mit Prompt-Cache** — die Zahlen gleich, nur `cached_tokens` unterschiedlich:
+
+```
+  (b) blocking usage   = {"completion_tokens":24,"prompt_tokens":8,"total_tokens":32,"prompt_tokens_details":{"cached_tokens":0}}
+  (b) last-chunk usage = {"completion_tokens":24,"prompt_tokens":8,"total_tokens":32,"prompt_tokens_details":{"cached_tokens":7}}
+  (b) EQUAL=false
+```
+
+**(b) Gegenprobe mit `cache_prompt:false`** — vollständig gleich:
+
+```
+  (b) blocking usage   = {"completion_tokens":24,"prompt_tokens":8,"total_tokens":32,"prompt_tokens_details":{"cached_tokens":0}}
+  (b) last-chunk usage = {"completion_tokens":24,"prompt_tokens":8,"total_tokens":32,"prompt_tokens_details":{"cached_tokens":0}}
+  (b) EQUAL=true
+```
+
+**(c) mit Prompt-Cache** — gleiche Tokens und Ids, Werte minimal verschieden (warme KV-Wiederverwendung
+ändert die Summationsreihenfolge), und im dritten Eintrag kippt der dritte `top_logprobs`-Platz
+(`" #"` gegen `" return"`):
+
+```
+blocking [0] {"id":13,"token":"\n","bytes":[10],"logprob":-0.1300244927406311, …}
+streamed [0] {"id":13,"token":"\n","bytes":[10],"logprob":-0.13420109450817108, …}
+blocking [2] {"id":565,"token":" if", …,"logprob":-0.36814239621162415,"top_logprobs":[… ," #" …]}
+streamed [2] {"id":565,"token":" if", …,"logprob":-0.3986988961696625, "top_logprobs":[… ," return" …]}
+```
+
+**(c) Gegenprobe mit `cache_prompt:false`** — die ersten drei Einträge **zeichengleich**, in beiden
+Prompt-Formen:
+
+```
+=== control text prompt
+  [0] identisch=True   [1] identisch=True   [2] identisch=True
+  alle drei identisch = True
+=== control token-id prompt
+  [0] identisch=True   [1] identisch=True   [2] identisch=True
+  alle drei identisch = True
+```
+
+**(d) Prompt als Token-Ids.** `LlamaModel.encode("def fibonacci(n):")` ergibt
+`[822,18755,265,21566,29898,29876,1125]` (n=7). Derselbe Text wie in (a), `EQUAL=true`, `usage`
+`prompt_tokens: 7`; mit `cache_prompt:false` auch `usage` und Logprobs gleich. Wichtig: die
+gestreamte Anfrage mit Token-Id-Prompt wird **angenommen** — genau das, was F186 reparierte.
+
+**(e) zwei Prompts** — beide Wege `400`, die Meldung nennt die Zahl:
+
+```
+  (e) blocking status=400 body={"error":{"message":"a request takes one prompt, this one has 2 prompts; send them as separate requests","type":"invalid_request_error","code":null}}
+  (e) streamed status=400 body={"error":{"message":"a request takes one prompt, this one has 2 prompts; send them as separate requests","type":"invalid_request_error","code":null}}
+  (e) blocking mentions 2 prompts=true
+  (e) streamed mentions 2 prompts=true
+```
+
+Nebenbeobachtungen: `contentType` ist blockierend `application/json; charset=utf-8` und gestreamt
+`text/event-stream; charset=utf-8`; alle 25 Chunks eines Streams tragen **eine** Id
+(`chatcmpl-…`, 32 Zeichen), 24 davon einen `logprobs.content`-Eintrag, der letzte das `usage` und
+ein textuelles `finish_reason`; `model` ist in beiden Wegen der Modellpfad, weil die Probe kein
+`modelId` setzt, das vom Standard abweicht (dazu M5).
+
+**Folgerung:** Die Hypothese ist bestätigt. (a), (d) und (e) ohne Vorbehalt; (b) und (c) ebenfalls,
+sobald der Prompt-Cache ausgeschaltet ist — die beobachtete Abweichung ist allein Cache-Wirkung und
+kein Unterschied zwischen den beiden Wegen. Der gestreamte Weg liefert damit denselben Text,
+dieselben Zahlen und dieselben Wahrscheinlichkeiten wie der blockierende und nimmt den
+Token-Id-Prompt an. Wer das Paar künftig vergleicht, muss `cache_prompt:false` setzen oder beide
+Seiten gleich warm fahren, sonst vergleicht er den Cache mit.
+
+---
+
+## M3 F183: Anthropic `stop_sequence` mit einem echten Stoppwort
+
+**Ziel.** Mit dem Tool-Modell (`DEFAULT_TOOL_MODEL_PATH`, Qwen2.5-1.5B) über `OpenAiCompatServer`:
+
+```json
+{"model": "m", "max_tokens": 64, "temperature": 0, "stop_sequences": ["5"],
+ "messages": [{"role": "user", "content": "Count from 1 to 10, separated by commas."}]}
+```
+
+an `/v1/messages`, einmal blockierend, einmal mit `"stream": true`. Aufzeichnen: `stop_reason`,
+`stop_sequence`, der Text; im Stream das `message_delta`-Ereignis. Dazu dieselbe Anfrage ohne
+`stop_sequences` (erwartet `stop_sequence: null`) und eine, die an `max_tokens` endet (`max_tokens: 3`,
+erwartet `stop_reason: max_tokens`, `stop_sequence: null`). Prüfen, dass `__verbose` in keiner Antwort und
+keinem Ereignis auftaucht.
+
+**Hypothese.** `end_turn` mit `stop_sequence: "5"`, Text endet vor der 5; die beiden Gegenproben wie erwartet.
+
+**Ergebnis:** Alle drei Anfragen `200`, blockierend `application/json; charset=utf-8`, gestreamt
+`text/event-stream; charset=utf-8`. Wörtlich:
+
+**Mit `stop_sequences: ["5"]`** — der Text endet vor der 5:
+
+```
+BLOCKING body={"id":"msg_6dbc1d7e","type":"message","role":"assistant","model":"m",
+ "content":[{"type":"text","text":"1, 2, 3, 4, "}],
+ "usage":{"input_tokens":42,"output_tokens":13,"cache_read_input_tokens":0},
+ "stop_reason":"end_turn","stop_sequence":"5"}
+STREAMED message_delta={"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":"5"},
+ "usage":{"input_tokens":1,"output_tokens":13,"cache_read_input_tokens":41}}
+STREAMED text=[1, 2, 3, 4, ]
+```
+
+**Ohne `stop_sequences`** — die Zählung läuft durch, `stop_sequence` ist `null`:
+
+```
+BLOCKING text=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+BLOCKING stop_reason="end_turn" stop_sequence=null
+STREAMED message_delta={"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},
+ "usage":{"input_tokens":1,"output_tokens":30,"cache_read_input_tokens":41}}
+```
+
+**Mit `max_tokens: 3`**:
+
+```
+BLOCKING text=[1, ]
+BLOCKING stop_reason="max_tokens" stop_sequence=null
+STREAMED message_delta={"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null},
+ "usage":{"input_tokens":1,"output_tokens":3,"cache_read_input_tokens":41}}
+```
+
+`__verbose` kommt in **keiner** der sechs Antworten und in keinem Ereignis vor (je `has
+__verbose=false` über den vollständigen Körper bzw. den vollständigen SSE-Strom geprüft, nicht nur
+über einzelne Felder).
+
+**Folgerung:** Die Hypothese ist in allen drei Fällen exakt bestätigt — `end_turn` mit
+`stop_sequence: "5"` und ein Text, der vor dem Stoppwort endet; `null` ohne Stoppwort; `max_tokens`
+mit `null` am Budgetende; blockierend und gestreamt tragen dasselbe Paar. F183 ist mit einem echten
+Stoppwort belegt.
+
+Eine Beobachtung ohne Befundcharakter: das `usage` der gestreamten Anfrage zählt `input_tokens: 1`
+bei `cache_read_input_tokens: 41`, weil die identische Anfrage unmittelbar nach der blockierenden
+läuft und den warmen Prompt-Cache trifft — dieselbe Cache-Wirkung wie in M2(b), nicht ein
+Unterschied der beiden Wege.
+
+---
+
+## M4 F182: ein gestreamter Fehler vor dem ersten Chunk mit dem echten Modell
+
+**Ziel.** Ein Prompt über der Kontextgröße ist bei upstream `400` (`exceed_context_size_error`), gestreamt
+wie nicht gestreamt. Mit `OpenAiCompatServer` über `DRAFT_MODEL_PATH` und `setCtxSize(256)` eine
+Chat-Anfrage mit ~2000 Wörtern Inhalt an alle fünf Streaming-Routen (`/v1/chat/completions`,
+`/v1/completions`, `/api/chat`, `/v1/messages`, `/v1/responses`, jeweils mit `"stream": true`).
+Aufzeichnen: HTTP-Status, `Content-Type`, Körper.
+
+**Hypothese.** Überall `400` mit JSON-Körper im Fehlerformat der Route, kein SSE.
+
+**Ergebnis:** Prompt 2000 Wörter / 13 789 Zeichen, Modell `AMD-Llama-135m-code.Q2_K.gguf` mit
+`setCtxSize(256)`. **Alle fünf Routen antworten `400` mit JSON, kein SSE** — geprüft wurde nicht nur
+der Status, sondern auch, dass der Körper kein `data: ` und kein `event: ` enthält:
+
+| Route | Status | `Content-Type` | SSE? |
+|---|---|---|---|
+| `/v1/chat/completions` | 400 | `application/json; charset=utf-8` | nein |
+| `/v1/completions` | 400 | `application/json; charset=utf-8` | nein |
+| `/api/chat` | 400 | `application/json; charset=utf-8` | nein |
+| `/v1/messages` | 400 | `application/json; charset=utf-8` | nein |
+| `/v1/responses` | 400 | `application/json; charset=utf-8` | nein |
+
+Die Körper, je im Fehlerformat der Route:
+
+```
+/v1/chat/completions {"error":{"message":"request (5818 tokens) exceeds the available context size (256 tokens), try increasing it","type":"invalid_request_error","code":null}}
+/v1/completions      {"error":{"message":"request (5791 tokens) exceeds the available context size (256 tokens), try increasing it","type":"invalid_request_error","code":null}}
+/api/chat            {"error":"request (5818 tokens) exceeds the available context size (256 tokens), try increasing it"}
+/v1/messages         {"type":"error","error":{"type":"invalid_request_error","message":"request (5818 tokens) exceeds the available context size (256 tokens), try increasing it"}}
+/v1/responses        {"error":{"message":"request (5818 tokens) exceeds the available context size (256 tokens), try increasing it","type":"invalid_request_error","code":null}}
+```
+
+Gegenprobe, dieselbe Anfrage **ohne** `"stream"` an `/v1/chat/completions`: `400`,
+`application/json; charset=utf-8`, Körper zeichengleich mit der gestreamten Zeile oben.
+
+**Folgerung:** Die Hypothese ist vollständig bestätigt. Der gestreamte Weg verhält sich wie der nicht
+gestreamte — Status, `Content-Type` und Körper sind gleich —, und keine Route schickt ein
+SSE-Vorspiel, bevor sie den Fehler meldet. Dass `/v1/completions` 5791 statt 5818 Tokens nennt, ist
+der fehlende Chat-Template-Rahmen, nicht eine Abweichung. F182 ist mit dem echten Modell auf allen
+fünf Routen belegt.
+
+---
+
+## M5 F185: `id` und `model` über beide Server
+
+**Ziel.** (a) `OpenAiCompatServer` mit `--model-id my-model` (Kommandozeile über `ServerLauncher
+--jllama-openai-compat`): `/v1/models`, eine blockierende Chat-Antwort und ein Stream; jedes `id` beginnt
+mit `chatcmpl-`, alle Chunks eines Streams tragen dieselbe, `model` ist überall `my-model`. (b) Dasselbe
+ohne `--model-id` (erwartet: der Dateiname). (c) `NativeServer` mit `--alias my-model` als Gegenprobe gegen
+upstreams eigenen Server im selben Prozess.
+
+**Ergebnis:** Je ein echtes Kind-JVM über `ServerLauncher`, Modell `AMD-Llama-135m-code.Q2_K.gguf`,
+`-c 512 -ngl 0`; der Port wird aus der Startmeldung des Kindes gelesen und mit `/health` bestätigt.
+
+**(a) `--jllama-openai-compat … --model-id my-model`** — wie erwartet:
+
+```
+GET /v1/models  {"object":"list","data":[{"id":"my-model","object":"model","created":1792447453,"owned_by":"llama.cpp","ftype":"Q2_K - Medium"}]}
+blocking id=chatcmpl-14XgXnJnNaDyD6DdDBXV6g5uuEY4N9Q6  startsWith chatcmpl-=true
+blocking model=my-model  == requested=true
+streamed chunks=8 ids=[chatcmpl-1CHN9eEjZyRnHKJdRENc6eBH7pRXQRn7] models=[my-model]
+streamed one id=true  id startsWith chatcmpl-=true
+```
+
+Also: jede `id` beginnt mit `chatcmpl-`, alle acht Chunks eines Streams tragen **eine** Id, und
+`model` ist überall `my-model`.
+
+**(c) `NativeServer --alias my-model`** als Gegenprobe gegen upstreams eigenen Server — deckungsgleich:
+
+```
+GET /v1/models  … "data":[{"id":"my-model","aliases":["my-model"], … }]
+blocking id=chatcmpl-fqJZm8uzg7akXK4lPIOnH9br0hNIYpqY  startsWith chatcmpl-=true
+blocking model=my-model  == requested=true
+streamed chunks=8 ids=[chatcmpl-EWLKYE2i6pFSMWGhlXs5JukSRzbxDHjk] models=[my-model]
+```
+
+**(b) ohne `--model-id` — hier weicht der Lauf ab, und das ist ein Befund.**
+
+**Befund B2 (Fundbuch: F189): ohne `--model-id` benennen `/v1/models` und die Antworten dasselbe Modell verschieden.**
+
+```
+GET /v1/models  {"object":"list","data":[{"id":"AMD-Llama-135m-code.Q2_K.gguf", … }]}
+blocking model=X:\Privat\OpenSource\github\java-llama.cpp\models\AMD-Llama-135m-code.Q2_K.gguf
+streamed models=[X:\Privat\OpenSource\github\java-llama.cpp\models\AMD-Llama-135m-code.Q2_K.gguf]
+```
+
+`/v1/models` meldet den **Dateinamen** — genau was die Usage-Zeile verspricht („`--model-id `
+Model id reported by /v1/models (default: file name)") und was
+`OpenAiServerCli.ParsedArgs.getModelId()` aus `Paths.get(modelPath).getFileName()` ableitet. Die
+Antworten melden dagegen den **vollen absoluten Pfad**, weil das `model`-Feld der Antwort nicht aus
+der Konfiguration kommt, sondern aus llama.cpps `--alias`, und das wird nur unter einer Bedingung
+gesetzt (`OpenAiServerCli.toModelParameters()`):
+
+```java
+if (modelId != null) {
+    // the answers name the model as llama.cpp knows it; this keeps them equal to /v1/models
+    params.setAlias(modelId);
+}
+```
+
+Der Kommentar nennt die Absicht — Antworten und `/v1/models` gleich halten —, aber der Zweig läuft
+nur, wenn `--model-id` wirklich übergeben wurde. Ohne das Flag bleibt der Alias leer und upstream
+setzt den Modellpfad ein. (c) zeigt, dass `--alias` die eine wirksame Stelle ist: damit stimmen beide
+Flächen. Die `id`-Hälfte von F185 ist davon **nicht** betroffen — `chatcmpl-` und eine Id pro Stream
+gelten in allen drei Fällen.
+
+Wirkung: ein Client, der `/v1/models` liest und das `model`-Feld der Antwort dagegen hält — oder der
+den aus `/v1/models` gelesenen Namen in die nächste Anfrage zurückschreibt —, sieht ohne
+`--model-id` zwei verschiedene Namen. Zusätzlich steht dann ein absoluter lokaler Pfad in jeder
+Antwort, auch in einer über das Netz.
+
+Dieselbe Trennung erklärt eine Beobachtung aus M2: die dortige Probe setzt `modelId("m")` direkt über
+`OpenAiServerConfig` (nicht über die CLI), und die Antworten tragen trotzdem den Modellpfad — weil
+nur die CLI `setAlias` aufruft. Über die eingebettete API ist das Feld also gar nicht einstellbar.
+
+**Folgerung:** (a) und (c) bestätigen F185 vollständig. (b) wich ab: der Vorgabewert griff nur für
+`/v1/models`, nicht für das `model`-Feld der Antworten, sodass die im Code ausgesprochene Zusage
+„keeps them equal to /v1/models" ohne das Flag nicht galt.
+
+### B2 (F189) ist behoben (auf Weisung, nach der Messung)
+
+**Warum der Pfad überhaupt erschien, nachgelesen statt vermutet.** Upstreams Kette für den Modellnamen
+(`tools/server/server-context.cpp`) ist Alias → `common_params_model::get_name()` → Dateiname, und
+`get_name()` lautet:
+
+```cpp
+std::string get_name() const {
+    if (!hf_repo.empty())     { return hf_repo; }
+    if (!docker_repo.empty()) { return docker_repo; }
+    return path;
+}
+```
+
+Für eine lokale Datei gibt Schritt 2 also den **Pfad** zurück, womit Schritt 3 — upstreams eigener
+Kommentar „fallback: derive model name from file name" — unerreichbar ist. Ohne Alias nennt der Server
+darum den Pfad, auch der von upstream selbst.
+
+**Der Fix** ist eine Zeile in `OpenAiServerCli.toModelParameters()`: der Alias wird **immer** gesetzt,
+aus `getModelId()` statt nur aus einem übergebenen `modelId`. Damit trägt der Vorgabewert dieselbe
+Zusage wie das Flag, und der eine Name, den der Server bewirbt, ist der, den llama.cpp benutzt.
+
+**End-to-end belegt** (Wegwerf-Probe, danach entfernt), derselbe Fall wie (b), ohne `--model-id`:
+
+```
+  expected id (file name)=AMD-Llama-135m-code.Q2_K.gguf
+  GET /v1/models id = AMD-Llama-135m-code.Q2_K.gguf
+  blocking model    = AMD-Llama-135m-code.Q2_K.gguf
+  blocking id       = chatcmpl-rkhNFoYimoN16qjlHGQNTFGFnWhvXRxw
+  streamed models   = [AMD-Llama-135m-code.Q2_K.gguf]
+  AGREE blocking == /v1/models : true
+  AGREE streamed == /v1/models : true
+  id is chatcmpl- and one per stream : true
+  no absolute path in the answer     : true
+```
+
+Vorher nannten dieselben drei Stellen den vollen absoluten Pfad; der lokale Pfad steht jetzt auch
+nicht mehr in jeder Antwort über das Netz.
+
+**Wächter:** `OpenAiServerCliTest.theAdvertisedModelIdBecomesTheModelsAliasWithAndWithoutTheFlag` —
+vorher hieß der Test `anExplicitModelIdBecomesTheModelsAlias` und schrieb das Gegenteil fest (ohne
+Flag *kein* `--alias`); er prüft nun beide Fälle und dass Alias und `getModelId()` derselbe String
+sind.
+
+**Was bewusst offen bleibt: der eingebettete Weg.** `OpenAiCompatServer(model, config)` bekommt ein
+schon geladenes Modell, kann also kein `--alias` mehr setzen — die Beobachtung aus M2 (dort steht der
+Pfad im `model`-Feld, obwohl die Probe `modelId("m")` setzt) gilt weiter. Das ist jetzt im Javadoc von
+`OpenAiServerConfig.getModelId()` gesagt, mit dem Hinweis, dass ein einbettender Aufrufer sein Modell
+mit `ModelParameters.setAlias(…)` laden muss. Das `model`-Feld der Antwort nachträglich umzuschreiben
+wäre die Alternative; sie würde über die Engine etwas anderes behaupten als diese getan hat und würde
+von `NativeServer` im Attach-Modus ohnehin nicht nachvollzogen — daher nicht getan, sondern benannt.
+
+---
+
+## M6 F67 / Patch 0021: LoRA-Wechsel mit dem echten Adapterpaar
+
+**Ziel.** J9 des vorigen Auftrags mit dem Patch wiederholen. Paar aus llama.cpps Server-Tests:
+`ggml-org/stories15M_MOE` → `stories15M_MOE-F16.gguf` und `moe_shakespeare15M.gguf`. Greedy
+(`temperature 0, top_k 1, seed 42`, `cache_prompt true`, Slot 0, `setParallel(1)`, Kontext 512), Prompt
+„Look in thy glass“, 32 Tokens. Zwei Durchgänge: mit `setCacheRamMib(0)` und mit der Voreinstellung.
+
+Ablauf je Durchgang: frisches Modell ohne Adapter (Referenz A); mit Adapter Skala 1, kalt und warm
+(Referenz B); `setLoraAdapter(0, 0f)` und sofort dieselbe Anfrage auf dem warmen Slot; dann
+`setLoraAdapter(0, 1f)` und wieder dieselbe Anfrage; dann eine Anfrage mit eigenem `"lora": [{"id": 0,
+"scale": 0}]`, gefolgt von einer ohne.
+
+**Hypothese.** Mit `setCacheRamMib(0)`: nach `0f` gleich A, nach `1f` gleich B, nach der Anfrage mit eigenen
+Adaptern die nächste gleich B. Mit der Voreinstellung kann der Host-Speicher-Cache (Javadoc von
+`setLoraAdapters`) abweichen — das ist der offene Rest, genau aufzeichnen.
+
+**Ergebnis:** Zuerst ein Befund, der den Ablauf änderte, dann die Messung selbst.
+
+**Befund B1 (Fundbuch: F187): `ModelParameters.addLoraScaledAdapter` ist gegen den Pin unbrauchbar.** Der erste
+Versuch, Referenz B über `addLoraScaledAdapter(ADAPTER, 1.0f)` zu laden, scheiterte:
+
+```
+net.ladenthin.llama.exception.LlamaException: Failed to parse model parameters
+	at net.ladenthin.llama.LlamaModel.loadModel(Native Method)
+```
+
+Die Methode rendert `putString(ModelOption.LORA_SCALED, fname + "," + scale)`, also
+`--lora-scaled ,`. Upstream liest den Wert aber als `FNAME:SCALE,...` —
+`common/arg.cpp` am Pin:
+
+```
+{"--lora-scaled"}, "FNAME:SCALE,...",
+"path to LoRA adapter with user defined scaling (format: FNAME:SCALE,...)\nnote: use comma-separated values",
+[](common_params & params, const std::string & value) {
+    for (const auto & item : parse_csv_row(value)) {
+        auto parts = string_split(item, ':');
+        if (parts.size() != 2) { throw std::invalid_argument("lora-scaled format: FNAME:SCALE"); }
+```
+
+Das Komma ist seit **b7448** der Listentrenner, nicht der Feldtrenner: `487674fbb` („common: fix
+--override-kv to support comma-separated values", upstream #18056, 2025-12-17), später erweitert um
+`07fbe19f1` (b7648, „arg: use CSV escape style for multiple-value args", #18643). Der Pin ist
+b11639, die Methode ist also seit rund 4200 Builds tot. **`addControlVectorScaled` hat dieselbe
+Form** (`fname + "," + scale`) und `--control-vector-scaled` dasselbe upstream-Format, ist also
+gleichermaßen betroffen. `setOverrideKv(String)` übergibt den Wert unverändert, liegt damit beim
+Aufrufer.
+
+Abgedeckt ist das nur von `ModelParametersTest#testAddLoraScaledAdapter`, das genau das falsche
+Format festschreibt:
+
+```java
+ModelParameters p = new ModelParameters().addLoraScaledAdapter("adapter.bin", 0.5f);
+assertThat(p.parameters.get("--lora-scaled"), is("adapter.bin,0.5"));
+```
+
+Das ist die Klasse, die CLAUDE.md für die Wire-Name-Registries beschreibt — ein Test, der die
+String-Abbildung prüft und darum grün bleibt, während das Flag tot ist. Die Registry-Prüfung
+`test_model_flags.cpp` sieht es nicht: sie hält den **Namen** gegen die Optionstabelle des Servers,
+nicht das **Werteformat**.
+
+Alle vier Formen gegen den echten Parser (je ein Kind-JVM über `ServerLauncher` → `NativeServer`,
+der llama-server-argv unverändert weitergibt):
+
+```
+1. --lora-scaled ,1.0   (was addLoraScaledAdapter heute sendet)
+   -> error while handling argument "--lora-scaled": invalid stof argument      Server startet nicht
+2. --lora-scaled :1.0   (upstream-Format, absoluter Windows-Pfad)
+   -> error while handling argument "--lora-scaled": lora-scaled format: FNAME:SCALE   Server startet nicht
+3. --lora-scaled :1.0   (upstream-Format, relativer Pfad)
+   -> 0.00.202.912 I srv  llama_server: listening on http://127.0.0.1:64459     angenommen
+4. --lora               (Gegenprobe, Skala 1.0 per Vorgabe)
+   -> 0.00.202.540 I srv  llama_server: listening on http://127.0.0.1:61766     angenommen
+```
+
+Form 2 ist bemerkenswert: **auch das korrekte Doppelpunkt-Format trägt auf Windows keinen absoluten
+Pfad.** `string_split` trennt an *jedem* Doppelpunkt (`common/common.h`), also ergibt
+`X:\…\a.gguf:1.0` drei Teile und nicht zwei. Eine Korrektur am Projekt von `,` auf `:` würde auf
+Linux greifen, auf Windows mit Laufwerksbuchstaben weiter scheitern — das ist eine
+upstream-Einschränkung, keine Projektentscheidung. (Nebenbei: der Parse-Fehler endet den Prozess mit
+**Exit 0** nach der Usage-Ausgabe.)
+
+Gemessen wurde darum mit `addLoraAdapter(ADAPTER)` (`--lora`, Skala 1.0 per Vorgabe) — genau die
+Skala, die M6 verlangt.
+
+**Die Messung.** Modell `stories15M_MOE-F16.gguf`, Adapter `moe_shakespeare15M.gguf`, Prompt „Look in
+thy glass", 32 Tokens, `temperature 0, top_k 1, seed 42, cache_prompt true, id_slot 0`,
+`setParallel(1)`, Kontext 512. Der Server meldet den Adapter als
+`[{"id":0,"path":"…moe_shakespeare15M.gguf","scale":1.0,…}]`.
+
+| Schritt | `cacheRamMib(0)` | Voreinstellung |
+|---|---|---|
+| A ohne Adapter | `was a little girl. She was only three years old …` | gleich |
+| B Adapter 1.0, kalt | `in love of the eye:\nThat's when when the eye see thy on the sun's shining bright. …` | gleich |
+| B′ Adapter 1.0, warm | = B | = B |
+| C nach `setLoraAdapter(0, 0f)` | **= A** | **= A** |
+| D nach `setLoraAdapter(0, 1f)` | **= B** | **= B** |
+| E Anfrage mit eigenem `lora` Skala 0 | **= A** | **= A** |
+| F nächste Anfrage, Server-Adapter 1.0 | **= B** | **= B** |
+
+Wörtlich aus dem Durchgang mit der Voreinstellung — dem, den der Auftrag als offenen Rest markiert:
+
+```
+  C  (setLoraAdapter(0,0f), warm slot)   = [ was a little girl. She was only three years old …]
+     C == A (expected) : true
+     C == B (defect)   : false
+  D  (setLoraAdapter(0,1f), warm slot)   = [ in love of the eye: …]
+     D == B (expected) : true
+     D == A (defect)   : false
+  E  (request lora scale 0)              = [ was a little girl. …]
+     E == A (expected) : true
+  F  (next request, server adapters 1.0) = [ in love of the eye: …]
+     F == B (expected) : true
+     F == A (defect)   : false
+```
+
+**Folgerung:** Die Messung bestätigt Patch 0021 mit dem echten Adapterpaar, und sie schließt den
+offenen Rest: **der Host-Speicher-Cache weicht nicht ab.** Beide Durchgänge sind Schritt für Schritt
+gleich — ein Adapterwechsel am warmen Slot wird nie aus dem alten Cache beantwortet, in keiner der
+beiden Cache-Einstellungen, auch nicht nach einer Anfrage mit eigenen Adaptern. Der Vorbehalt im
+Javadoc von `setLoraAdapters` lässt sich mit dieser Messung nicht nachstellen.
+
+Offen blieb damit nicht Patch 0021, sondern Befund **B1**: der skalierte Adapterpfad über
+`ModelParameters` lud am Pin überhaupt kein Modell, und der einzige Test darüber schrieb das falsche
+Format fest. Die Messung musste deshalb auf `--lora` ausweichen.
+
+### B1 (F187) und B4 (F188) sind behoben (auf Weisung, nach der Messung)
+
+Beim Fixen kam ein weiterer Fehler derselben Familie mit heraus, **B4** (Fundbuch: F188): `putString` ist
+`map.put(option, value)` auf einer `LinkedHashMap`, also **überschreibt** ein zweiter
+`addLoraAdapter` / `addLoraScaledAdapter` / `addControlVector` / `addControlVectorScaled` den ersten —
+während alle vier im Javadoc „can be repeated to use multiple adapters" versprechen. Upstream liest
+genau diese Optionen seit #18056 / #18643 als **eine** komma-separierte Liste, Akkumulation ist also
+das, was sie zusammensetzt.
+
+Der Fix, in `llama/src/main/java/net/ladenthin/llama/parameters/`:
+
+- **`CliParameters.appendCsvValue(option, value)`** neu: hängt einen Wert an, statt zu ersetzen, und
+  schreibt ihn als CSV-Feld — in Anführungszeichen mit verdoppelten inneren Anführungszeichen, sobald
+  er ein Komma oder ein Anführungszeichen trägt, weil upstreams `parse_csv_row` ein öffnendes
+  Anführungszeichen nur am Feldanfang erkennt und ein Pfad mit Komma sonst gar nicht übergeben werden
+  kann.
+- **Die vier Methoden** nutzen ihn; die beiden skalierten rendern `FNAME:SCALE` statt `FNAME,SCALE`.
+- **Ein Pfad mit Doppelpunkt wird laut abgelehnt** (`IllegalArgumentException`), statt den Ladevorgang
+  später mit „Failed to parse model parameters" scheitern zu lassen. Die Meldung nennt die Ursache und
+  die zwei Auswege. Das ist die Grenze aus Form 2 oben: sie ist upstreams, nicht unsere, und lässt
+  sich hier nicht wegkorrigieren — ein `,`→`:`-Tausch allein hätte auf Windows weiter nicht geladen.
+
+**End-to-end belegt** mit demselben Adapterpaar (Wegwerf-Probe, danach entfernt):
+
+```
+### B1 absolute path refused: --lora-scaled reads its value as FNAME:SCALE and splits on every colon,
+    so a path containing one cannot be expressed: X:\…\moe_shakespeare15M.gguf
+    -- pass a relative path, or load it unscaled and set the scale after the load
+  rendered argv for the relative path: [, --fit, on, --lora-scaled, ..\models\moe_shakespeare15M.gguf:0.5,second.gguf:0.25]
+  A  (no adapter)                  = [ was a little girl. She was only three years old …]
+  B  (--lora, scale 1.0)           = [ in love of the eye: …]
+  C  (--lora-scaled :1.0)     = [ in love of the eye: …]
+     adapters: [{"id":0,"path":"..\\models\\moe_shakespeare15M.gguf","scale":1.0,…}]
+     C == B (expected) : true
+  D  (--lora-scaled :0.0)     = [ was a little girl. …]
+     adapters: [{"id":0,"path":"..\\models\\moe_shakespeare15M.gguf","scale":0.0,…}]
+     D == A (expected, scale carried) : true
+     D == B (defect, scale ignored)   : false
+```
+
+Also: die Option **lädt überhaupt erst** (vorher `LlamaException: Failed to parse model parameters`),
+zwei skalierte Adapter sammeln sich zu einem Wert, und die Skala wirkt — `:1.0` ergibt Referenz B,
+`:0.0` ergibt Referenz A, und der Server meldet die Skala zurück. Damit ist auch das nachgeholt, was
+die Messung nicht konnte: eine **andere** Skala als 1.0 über `ModelParameters`.
+
+**Wächter**, in `ModelParametersTest`: das neue `FNAME:SCALE`-Format (die beiden Tests, die das alte
+festschrieben, sind umgestellt), die Akkumulation aller vier Optionen, das CSV-Quoting eines Pfads mit
+Komma und Anführungszeichen, und die Ablehnung eines Doppelpunkt-Pfads samt der Meldung — mit einem
+relativen Pfad als Gegenprobe im selben Test. 97 Tests der Klasse grün.
+
+---
+
+## M7 F181: Fehlermeldungen mit einem 4-Byte-Zeichen auf Windows
+
+**Ziel.** `LlamaTrainerTest` läuft in CI auf Linux; der Windows-Weg (UTF-16-Dateinamen, `cmd`-Codepage)
+ist ungeprüft. Auf Windows: `mvn -B -f llama/pom.xml test -Dtest=LlamaTrainerTest`, dann eine Wegwerf-Probe,
+die `new LlamaModel(new ModelParameters().setModel("C:/tmp/fehlt-\uD83D\uDE00.gguf"))` und
+`NativeServer` mit demselben Pfad startet und die Meldung codepunktweise ausgibt.
+
+**Hypothese.** Beide Tests grün (nicht übersprungen); die Probe zeigt U+1F600 als einen Codepunkt, nichts
+abgeschnitten.
+
+**Ergebnis:** Beide Teile grün.
+
+**`LlamaTrainerTest` auf Windows** — die beiden Methoden liefen, nicht übersprungen (aus M1):
+
+```
+[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 -- in net.ladenthin.llama.LlamaTrainerTest
+testcase name="aFourByteCharacterInAPathComesBackIntactInTheErrorMessage"
+testcase name="aFourByteCharacterInTheTrainingTextReachesTheTrainer"
+```
+
+**Die Probe.** Umgebung des Test-JVM:
+
+```
+os=Windows 11 10.0 | file.encoding=UTF-8 | sun.jnu.encoding=Cp1252 | native.encoding=Cp1252 | java=21.0.12.1+1-LTS
+path asked for=[C:\Users\…\junit-4414633441307162606\fehlt-😀.gguf] codePointCount=72
+Paths.get round trip equal=true
+```
+
+`sun.jnu.encoding` ist also **Cp1252** und kann U+1F600 gar nicht darstellen — der interessante Fall.
+Die Meldung des Trainers, codepunktweise:
+
+```
+### M7 LlamaTrainer.finetune -> net.ladenthin.llama.exception.LlamaException
+  message=[failed to load model for training: C:\…\junit-4414633441307162606\fehlt-😀.gguf]
+  length(chars)=108 codePointCount=107
+  codepoints=… U+0066 U+0065 U+0068 U+006C U+0074 U+002D U+1F600 U+002E U+0067 U+0067 U+0075 U+0066
+  contains U+1F600 as ONE codepoint=true
+  mangled as U+00F0 U+009F U+0098 U+0080=false
+  truncated (ends in ".g" instead of ".gguf")=false
+  replacement char U+FFFD present=false
+  '?' mangling present=false
+  EQUALS expected=true
+```
+
+Die Codepunktfolge endet also auf `f e h l t - U+1F600 . g g u f`: ein Codepunkt für das Emoji,
+`.gguf` vollständig, und die Meldung ist zeichengleich mit dem erwarteten String. Die
+Modified-UTF-8-Verstümmelung, die F181 behob (U+00F0 U+009F U+0098 U+0080 und ein nach `.g`
+abgeschnittener Rest), tritt nicht auf. Dasselbe mit dem 4-Byte-Zeichen im **Trainingstext** statt im
+Pfad: Meldung unverstümmelt, Pfad korrekt.
+
+Zwei Nebenbeobachtungen, beide **kein** Befund:
+
+- `new LlamaModel(…)` mit demselben Pfad wirft `LlamaException: could not load model from given file
+  path` — die Meldung trägt den Pfad **gar nicht**, dieser Eintrittspunkt kann F181 also nicht
+  zeigen. Der Trainer ist die Stelle, an der der Pfad in der Meldung steht.
+- Die **nativen Logzeilen** auf der Konsole zeigen `fehlt-?.gguf`
+  (`gguf_init_from_file: failed to open GGUF file 'C:\…\fehlt-?.gguf'`). Das ist die Codepage der
+  `cmd`-Konsole beim Schreiben von stderr, nicht Datenverlust: die Java-sichtbare Meldung derselben
+  Anfrage trägt U+1F600 unverändert (oben). Wer die Konsole als Beleg nimmt, kommt zum falschen
+  Schluss.
+- `new NativeServer("--model", , "--port", "0").start()` kehrt ohne Ausnahme zurück. Das
+  ist der dokumentierte Vertrag — „Starts the native HTTP server … and returns immediately. The
+  server binds and begins serving `GET /health` before the model finishes loading; poll `/health` for
+  readiness" —, also erwartet und kein Befund.
+
+**Folgerung:** Die Hypothese ist bestätigt. Beide Trainer-Tests laufen auf Windows grün und sind
+nicht übersprungen, und die Probe zeigt U+1F600 als einen Codepunkt bei intaktem Rest, obwohl
+`sun.jnu.encoding=Cp1252` das Zeichen nicht darstellen kann. F181 trägt auf dem Windows-Weg.
+
+---
+
+## M8 F182 im Agenten: Atmosphere über das echte Modell
+
+**Ziel.** Seit F182 meldet der Agent einen Fehler vor dem ersten Chunk als Fehler des Zuges statt still zu
+enden. Lauf: `mvn -B -f llama-atmosphere-agent/pom.xml -Dllama.version=6.0.0-SNAPSHOT -Dllama.natives=none
+verify` gegen den installierten Kern (`mvn -pl llama,llama-platform -am install -DskipTests`), dann
+`AtmosphereToolLoopIntegrationTest` mit dem Tool-Modell (`-Dnet.ladenthin.llama.lib.path=`).
+Dazu ein Zug im Agenten mit einem Prompt über der Kontextgröße (`--ctx-size 256`): was zeigt die Konsole?
+
+**Ergebnis:** In drei Teilen — der modellfreie Lauf (mit einem Befund), der Tool-Loop mit dem echten
+Modell, und der Zug über der Kontextgröße.
+
+**(1) Modellfreier Lauf.** `mvn -f llama-atmosphere-agent/pom.xml -Dllama.version=6.0.0-SNAPSHOT
+-Dllama.natives=none verify` gegen den installierten Kern (`mvn -pl llama,llama-platform -am install
+-DskipTests`, `BUILD SUCCESS`):
+
+```
+[ERROR] Tests run: 276, Failures: 1, Errors: 1, Skipped: 60
+[INFO] BUILD FAILURE
+```
+
+Die 60 übersprungenen sind erwartet und dokumentiert (`ScreenUseCasesTest` 57 + `ReflowResizeTest` 3,
+die sich gegen das veröffentlichte JLine selbst abschalten, siehe `llama-atmosphere-agent/CLAUDE.md`).
+Der `Errors: 1` ist mein Aufruf: `AtmosphereToolLoopIntegrationTest` findet lokal sein GGUF (anders als
+in CIs modellfreiem Job) und läuft dann in `UnsatisfiedLinkError: No native library found`, weil
+`-Dllama.natives=none` keine Natives auf den Klassenpfad legt und ich `lib.path` erst in Teil (2)
+gesetzt habe. Kein Projektfehler, aber eine Folge, die lokal jeden trifft, der den modellfreien Lauf
+mit vorhandenen Modellen fährt.
+
+**Der `Failures: 1` ist ein Befund.**
+
+**Befund B3 (Fundbuch: F190): `AgentSessionTest.aLoopRunsItsStepsThroughTheFrontEndUntilTheMarker` ist auf Windows
+deterministisch rot.** Drei Läufe, drei Mal dieselbe Zeile:
+
+```
+Lauf 1 : [ERROR] Tests run: 13, Failures: 1, Errors: 0, Skipped: 0
+Lauf 2 : [ERROR] Tests run: 13, Failures: 1, Errors: 0, Skipped: 0
+Lauf 3 : [ERROR] Tests run: 13, Failures: 1, Errors: 0, Skipped: 0
+```
+
+```
+java.lang.AssertionError:
+Expected: a string containing "loop: done after 1 steps"
+     but: was "loop: write a poem
+loop: state in C:\Users\…\AGENT-LOOP.md, max 20 steps, budget 120 min
+── loop step 1/20 ──
+── loop step 2/20 ──
+── loop step 3/20 ──
+loop: no progress for 3 steps (nothing written, no tools used)"
+	at …AgentSessionTest.aLoopRunsItsStepsThroughTheFrontEndUntilTheMarker(AgentSessionTest.java:341)
+```
+
+und unmittelbar davor im Log:
+
+```
+WARN org.atmosphere.ai.llm.OpenAiCompatibleClient - LLM connection error, retrying in 500ms (attempt 1/3): HTTP/1.1 header parser received no bytes
+```
+
+Ursache, mit einer Probe belegt statt erschlossen. Der Test lässt das Skript-Backend
+`"working" + System.lineSeparator() + TaskLoop.SENTINEL` senden, und `ScriptedBackend.quote`
+(`llama-atmosphere-agent/src/test/.../ScriptedBackend.java`) escapt Anführungszeichen, Backslash und
+`\n` — **nicht `\r`**: der `switch` hat Zweige für `'"'`, `'\\'` und `'\n'` und schreibt alles andere
+unverändert durch.
+
+Auf Windows ist `System.lineSeparator()` `\r\n`, der Chunk trägt also ein rohes CR mitten im
+JSON-String. Die Probe zeigt beides nebeneinander:
+
+```
+os=Windows 11  System.lineSeparator() = CRLF
+### as the test builds it: "working" + System.lineSeparator() + SENTINEL
+  text   = working<0D><0A><>
+  chunk  = {… "delta":{"content":"working<0D>\n<>"} …}
+  raw CR in chunk  = 113
+  parses as JSON   = NO: JsonParseException: Illegal unquoted character ((CTRL-CHAR, code 13)): has to be escaped using backslash to be included in string value
+  TaskLoop.isComplete(text) = true
+### with LF only (what Linux gets)
+  text   = working<0A><>
+  chunk  = {… "delta":{"content":"working\n<>"} …}
+  raw CR in chunk  = -1
+  parses as JSON   = yes
+  TaskLoop.isComplete(text) = true
+```
+
+Der Chunk ist also auf Windows **kein gültiges JSON** (RFC 8259 verbietet unescapte Steuerzeichen
+U+0000–U+001F), die SSE-Antwort bricht ab, jeder Loop-Schritt bekommt nichts, und der
+Stillstandswächter beendet den Loop nach drei Schritten. **Die Sentinel-Erkennung ist nicht
+schuld** — `TaskLoop.isComplete` ist für beide Zeilenenden `true`.
+
+Es ist Testcode, kann also nichts an Nutzer ausliefern; die Wirkung ist, dass das Agentenmodul auf
+Windows nicht grün baut. Gesehen wurde es nie, weil die drei Agenten-Jobs nur auf Linux laufen.
+
+**B3 (F190) ist auf Weisung in diesem Lauf behoben** — eine Ausnahme von der Regel „nur messen", ausdrücklich
+beauftragt. Zwei Dinge dabei, die beim Messen auffielen:
+
+**Die Lücke war viel größer als das fehlende `\r`.** Beide `quote`-Fassungen in einem
+Einzeldatei-Programm gegen Jackson, auf dieser Maschine:
+
+```
+System.lineSeparator() = CRLF
+VORHER   control chars that are not JSON: 31/32   value altered: 0
+         namely: U+0000 U+0001 … U+000D … U+001F
+         System.lineSeparator() round trip: FAILS: Illegal unquoted character ((CTRL-CHAR, code 13))
+NACHHER  control chars that are not JSON:  0/32   value altered: 0
+         System.lineSeparator() round trip: ok
+```
+
+Von 32 Steuerzeichen erzeugten **31** ungültiges JSON; behandelt war allein `\n`. Der Wert wird in
+keiner Fassung verändert, es ging rein um Gültigkeit.
+
+**Der Fix** setzt die vier benannten Escapes `\r \t \b \f` dazu und — das Tragende — einen
+`default`-Zweig, der alles unter `0x20` als `\uXXXX` schreibt, damit ein Steuerzeichen, das niemand
+aufgelistet hat, die Lücke nicht wieder aufreißen kann. Der Test selbst bleibt unangetastet:
+`System.lineSeparator()` dort ist richtig, weil genau das CRLF überhaupt erst ausübt.
+
+**Wächter: `ScriptedBackendTest`** (neu, 3 Fälle) — jeder Chunk eines Text-Zuges muss als JSON
+parsen (mit `System.lineSeparator()`, CRLF, LF, einzelnem CR); **jedes** der 32 Steuerzeichen
+einzeln, also der `default`-Zweig und nicht die vier benannten Fälle; und der Text muss unverändert
+aus dem Chunk zurückkommen. Ohne so einen Wächter ist der Fehler unsichtbar, weil ein kaputter Chunk
+als abgebrochener Stream ankommt und die rote Zusicherung mehrere Schichten entfernt über die
+*Antwort* spricht.
+
+**Rot/grün im echten Build belegt, nicht nur in der Isolation.** Mit der alten `quote`-Fassung kurz
+zurückgesetzt:
+
+```
+[ERROR] Tests run: 13, Failures: 1, Errors: 0, Skipped: 0 -- in …AgentSessionTest
+[ERROR] Tests run:  3, Failures: 2, Errors: 1, Skipped: 0 -- in …ScriptedBackendTest
+[ERROR]   ScriptedBackendTest.aTextTurnCarryingTheLineSeparatorIsJsonOnEveryPlatform:44 System.lineSeparator(): chunk is not JSON: …
+[ERROR]   ScriptedBackendTest.everyControlCharacterSurvivesAsJson:60 U+0000: chunk is not JSON: …
+[ERROR]   ScriptedBackendTest.theTextComesBackOutOfTheChunkUnchanged:73 » JsonParse Illegal unquoted character ((CTRL-CHAR, code 13))
+```
+
+und mit dem Fix (die Datei danach byte-identisch wiederhergestellt), drei Läufe hintereinander:
+
+```
+Lauf 1/2/3: Tests run: 13, Failures: 0, Errors: 0, Skipped: 0 -- in …AgentSessionTest
+Lauf 1/2/3: Tests run:  3, Failures: 0, Errors: 0, Skipped: 0 -- in …ScriptedBackendTest
+```
+
+Und das ganze Modul, diesmal mit `lib.path`, also ohne den Aufruffehler aus Teil (1):
+
+```
+[INFO] Tests run: 281, Failures: 0, Errors: 0, Skipped: 60
+[INFO] BUILD SUCCESS
+```
+
+Die 60 Auslassungen sind unverändert die dokumentierten JLine-Fälle. `spotless:apply` hatte nichts zu
+ändern. Geprüft wurde außerdem, ob dieselbe Lücke woanders steckt: ein `grep` über alle handgerollten
+JSON-Quote-Helfer im Repository findet nur diese eine Stelle.
+
+**(2) Der Tool-Loop mit dem echten Modell.** Dieselbe Klasse mit
+`-Dnet.ladenthin.llama.lib.path=` und dem Tool-Modell:
+
+```
+[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 10.76 s -- in net.ladenthin.llama.atmosphere.AtmosphereToolLoopIntegrationTest
+[INFO] BUILD SUCCESS
+```
+
+mit den Methoden `plainChatStreamsAnAnswer`, `toolCallResultIsFedBackAndAnswered` und
+`multiRoundReadWriteReadLoopChangesTheFile` — nicht übersprungen.
+
+**(3) Der Zug über der Kontextgröße.** Vier Züge über `exec:java` (einmal `--ctx-size 256` wie im
+Auftrag, dazu eine brauchbare Gegenprobe, siehe unten), je `--plain --temperature 0 --ngl 0`:
+
+Zug 1, `--ctx-size 256` mit dem 2000-Wörter-Prompt — die Konsole zeigt:
+
+```
+(context nearly full — compacting first)
+(nothing to compact)
+srv    send_error: task id = 0, error: request (1897 tokens) exceeds the available context size (256 tokens), try increasing it
+srv          stop: cancel task, id_task = 0
+ERROR org.atmosphere.ai.llm.OpenAiCompatibleClient - LLM API error (400): {"error":{"message":"request (1897 tokens) exceeds the available context size (256 tokens), try increasing it","type":"invalid_request_error","code":null}}
+[error] org.atmosphere.ai.AiProviderException$TerminalProviderError: API returned 400: request (1897 tokens) exceeds the available context size (256 tokens), try increasing it
+AGENT_EXIT_OVER=1
+```
+
+Zug 2, `--ctx-size 256` mit dem **kurzen** Prompt, scheitert ebenso (`request (1483 tokens) exceeds
+the available context size (256 tokens)`) — **bei 256 sprengt schon der Systemprompt den Kontext**,
+der im Auftrag vorgeschlagene Wert ist für den Agenten also an sich zu klein und taugt nicht als
+Gegenprobe. Darum zwei weitere:
+
+Zug 3, `--ctx-size 4096`, kurzer Prompt — der Agent antwortet normal:
+
+```
+2 + 2 equals 4.
+AGENT_EXIT_CTRL=0
+```
+
+Zug 4, `--ctx-size 1024`, der 2000-Wörter-Prompt — wieder der Fehler des Zuges:
+
+```
+[error] org.atmosphere.ai.AiProviderException$TerminalProviderError: API returned 400: request (1897 tokens) exceeds the available context size (1024 tokens), try increasing it
+AGENT_EXIT_OVER2=1
+```
+
+**Folgerung:** Teil (3) bestätigt F182 im Agenten: ein Fehler vor dem ersten Chunk erscheint als
+`[error] … API returned 400: …` mit der vollständigen Meldung des Servers und beendet den Prozess mit
+Code 1, statt still zu enden — und Zug 3 zeigt im Gegenzug, dass derselbe Agent mit arbeitsfähigem
+Kontext normal antwortet, der Fehler also am Kontext hängt und nicht am Aufbau. Teil (2) bestätigt
+den Tool-Loop mit dem echten Modell. Teil (1) brachte Befund **B3** — das Agentenmodul baute auf
+Windows nicht grün, weil der Testharnisch 31 von 32 Steuerzeichen unescapt in seine JSON-Chunks
+schrieb —, und B3 ist auf Weisung in diesem Lauf behoben und mit einem Wächter versehen; das Modul
+ist damit auf Windows grün (281 Tests, 0 Fehler, 60 dokumentiert übersprungen).
+
+Zwei Dinge, die **kein** Befund sind: das `â€"` in einer früheren Lesung von
+`(context nearly full — compacting first)` war die Codepage meines Log-Lesens, nicht die Ausgabe des
+Agenten (als UTF-8 gelesen steht dort ein korrektes `—`); und `--ctx-size 256` als Vorschlag im
+Auftrag ist, wie oben, für den Agenten zu klein.
+
+---
+
+## M9 `llama-ffm` mit dem echten Modell
+
+**Ziel.** `mvn -B -f llama-ffm/pom.xml verify -Dnet.ladenthin.llama.ffm.model=models/AMD-Llama-135m-code.Q2_K.gguf
+-Dnet.ladenthin.llama.lib.path=<…/llama/src/main/natives/net/ladenthin/llama///cpu>` — neu ist
+`FfmLlamaModelIntegrationTest#oaiCompletionStreamEndsWithUsage` (F186).
+
+**Ergebnis:** Grün, nichts übersprungen. Die Toolchain greift wie vorgesehen — Maven läuft auf
+Temurin 21, das Modul auf Temurin 25:
+
+```
+[INFO] --- toolchains:3.3.0:toolchain (default) @ llama-ffm ---
+[INFO] Required toolchain: jdk [ version='[22,)' ]
+[INFO] Found matching toolchain for type jdk: JDK[C:\Users\berna\AppData\Local\Programs\jdk-25.0.4.1+1]
+[INFO] Toolchain in maven-compiler-plugin: JDK[…jdk-25.0.4.1+1]
+[INFO] Toolchain in maven-surefire-plugin: JDK[…jdk-25.0.4.1+1]
+```
+
+```
+[INFO] Tests run: 17, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.355 s -- in net.ladenthin.llama.ffm.FfmLlamaModelIntegrationTest
+[INFO] Tests run:  4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.512 s -- in net.ladenthin.llama.ffm.FfmNativeServerTest
+[INFO] Tests run:  9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.036 s -- in net.ladenthin.llama.ffm.JllamaNativeTest
+[INFO] Tests run: 30, Failures: 0, Errors: 0, Skipped: 0
+[INFO] BUILD SUCCESS
+```
+
+Der neue Test lief, aus dem surefire-XML belegt (eine Selbstabschaltung wäre hier der stille Fall,
+den der CI-Job eigens prüft):
+
+```
+name="oaiCompletionStreamEndsWithUsage" classname="net.ladenthin.llama.ffm.FfmLlamaModelIntegrationTest" time="0.036"
+```
+
+Aufruf: `mvn -f llama-ffm/pom.xml verify
+-Dnet.ladenthin.llama.ffm.model=models/AMD-Llama-135m-code.Q2_K.gguf
+-Dnet.ladenthin.llama.lib.path=llama/src/main/natives/net/ladenthin/llama/Windows/x86_64/cpu`. Der
+relative Modellpfad greift, weil `FfmTestSupport.modelPath()` auf `../models/…` zurückfällt.
+
+**Folgerung:** Der FFM-Weg trägt mit dem echten Modell auf Windows, und F186 ist auch dort belegt:
+`oaiCompletionStreamEndsWithUsage` verlangt `object: "text_completion"` auf jedem Chunk und ein
+`usage.completion_tokens > 0` auf dem letzten. Für diesen Punkt musste ein JDK 22+ her — auf der
+Maschine war keines, also wurde Temurin 25.0.4.1+1 (141 MB) installiert und ein
+`~/.m2/toolchains.xml` mit 21 und 25 angelegt, den es vorher nicht gab.
+
+---
+
+## M10 Gesamtlauf
+
+**Ziel.** Nach M1 … M9 einmal alles: `ctest` (erwartet 643/643) und
+`mvn -B -f llama/pom.xml test -Dnet.ladenthin.llama.test.ngl=0` mit dem ganzen Modellsatz, dazu
+`.github/verify-test-counts.sh` wie in CI. Zahlen (run / failures / errors / skipped) eintragen.
+
+**Ergebnis:** Beide Hälften grün.
+
+**`ctest`** gegen den statischen Build mit `-DBUILD_TESTING=ON` — die vom Auftrag erwartete Zahl:
+
+```
+100% tests passed, 0 tests failed out of 643
+Total Test time (real) = 147.01 sec
+```
+
+**Die Java-Suite**, `mvn -f llama/pom.xml test -Dnet.ladenthin.llama.test.ngl=0` mit dem ganzen
+Modellsatz:
+
+```
+[INFO] Tests run: 2151, Failures: 0, Errors: 0, Skipped: 18
+[INFO] BUILD SUCCESS
+[INFO] Total time:  08:48 min
+```
+
+**Die Zählprüfung wie in CI** (`.github/verify-test-counts.sh llama/target/surefire-reports
+--min-executed 1800`):
+
+```
+test counts verified: 2151 test(s) run, 18 skipped, 2133 executed across 139 class(es), none empty (floors: total 0, executed 1800)
+```
+
+| | |
+|---|---:|
+| run | 2151 |
+| failures | 0 |
+| errors | 0 |
+| skipped | 18 |
+| executed (run − skipped) | 2133 |
+| Klassen, keine leer | 139 |
+
+**Eine Falle beim Zählen, die hier zuschlug und protokolliert gehört:** der erste Aufruf meldete
+2158 über 146 Klassen. `mvn test` räumt `target/surefire-reports` nicht auf, also lagen dort noch die
+XML der sieben Wegwerf-Proben aus M2 … M7 — genau 7 Klassen und 7 Tests mehr. Die Zahlen oben sind
+nach dem Entfernen dieser Berichte, bestehen also nur aus dem Gesamtlauf. Wer die Zahl eines
+Gesamtlaufs übernimmt, muss das Verzeichnis vorher leeren oder `clean` fahren.
+
+**Die 18 Auslassungen, einzeln geprüft** — jede ist erklärt und für einen statischen Windows-Build
+erwartet, keine ist eine stumme Klasse (`none empty` oben):
+
+| Anzahl | Tests | Grund |
+|---:|---|---|
+| 8 | `BackendLoadTest` (alle) | `backend fixtures are committed for Linux only` |
+| 2 | `RouterModeIntegrationTest#chatCompletion_isProxiedToWorker`, `#models_listContainsLoadedModel` | `Router worker-relaunch test runs on Linux only` |
+| 2 | `ProcessRunnerTest#returnsStandardOutput`, `#commandThatDoesNotEndInTimeIsKilledAndReported` | `uname` bzw. `sleep` sind POSIX-Kommandos (`assumeFalse(WINDOWS, …)`) |
+| 2 | `CpuModuleIsolationTest#everyCpuModuleLoadsOnItsOwn`, `LlamaLoaderTest#extractionDirectoriesOfThisClasspathNameTheLibraryWithItsModules` | brauchen die **modulare** Auslage aus den Jars; dieser Build ist der statische, den der Loader direkt aus `src/main/natives` lädt |
+| 2 | `SystemOneIntegrationTest#aDecisionModelAnswersEveryQuestionType`, `#aDecisionModelRejectsAnUnknownQuestionType` | das Entscheidungsmodell ist nicht im CI-Satz (CLAUDE.md) |
+| 1 | `AudioInputIntegrationTest#audioInputProducesNonEmptyReply` | Audio-Modell + mmproj sind nicht im CI-Satz (CLAUDE.md) |
+| 1 | `LlamaModelTest#testLogStdout` | `@Disabled` |
+
+**Folgerung:** Der Gesamtlauf bestätigt, dass die Messungen M1 … M9 den Baum so hinterlassen haben,
+wie sie ihn gefunden haben: 643/643 in C++ und 2151/0/0/18 in Java, 2133 ausgeführt gegen eine
+Untergrenze von 1800, keine Klasse mit null Einträgen.
+
+### Nachlauf nach den Fixes
+
+Die Zahlen oben sind der Gesamtlauf **vor** den Behebungen von B1/B2/B4 (B3 war zu diesem Zeitpunkt
+schon drin). Danach wurde alles noch einmal gefahren, jedes Modul gegen den neu installierten Kern:
+
+| Lauf | Ergebnis |
+|---|---|
+| `ctest` (C++, unverändert — die Fixes sind reines Java) | 643/643 |
+| `mvn -f llama/pom.xml clean test` (ganzer Modellsatz) | **2154** / 0 / 0 / 18, 8:43 min |
+| `verify-test-counts.sh … --min-executed 1800` | 2154 run, 18 skipped, **2136 executed**, 139 Klassen, keine leer |
+| `spotbugs:check` wie im `code-style`-Job | `BugInstance size is 0`, `Error size is 0` |
+| `spotless:check` (Kern und Agentenmodul) | grün |
+| `llama-atmosphere-agent` (`verify` mit `lib.path`) | 281 / 0 / 0 / 60 |
+| `llama-ffm` (`verify` mit echtem Modell) | 30 / 0 / 0 / 0 |
+| `llama-langchain4j` (`verify`) | 55 / 0 / 0 / 7 |
+| `llama-kotlin` (`verify`) | 6 / 0 / 0 / 0 |
+
+2154 statt 2151 sind die drei neuen Tests in `ModelParametersTest` (Akkumulation, CSV-Quoting,
+Doppelpunkt-Ablehnung); `ScriptedBackendTest` steckt in den 281 des Agentenmoduls. Die acht
+Wegwerf-Proben der Messung und die zwei des Fix-Nachweises sind entfernt; was im Arbeitsbaum bleibt,
+ist diese Datei plus die Fixes zu B1, B2, B3 und B4 mit ihren Wächtern.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 347431003..dea5592c9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,8 +39,11 @@ the first two items break every existing consumer at upgrade.
    `setHfRepoV`/`setHfFileV`, `enableMlock`, `disableMmap` -- the Removed section names the
    replacement where one exists, e.g. `setLoadMode(LoadMode)`). The attach-mode server reads the
    endpoint toggles from `ModelParameters` now (`enableMetricsEndpoint()` & co.), not from its argv.
-4. **`NativeServer` listens on 9931 when no `--port` is given** (llama.cpp's new default);
-   `OpenAiCompatServer` stays on 8080. Pass `--port` where a fixed port matters.
+4. **Both servers listen on 9931 when no port is given**, llama.cpp's default since b11521:
+   `NativeServer` follows llama-server, and `OpenAiCompatServer` (`OpenAiServerConfig.DEFAULT_PORT`,
+   the `--port` default of its command line) moved from 8080 to the same port. Pass `--port` (or
+   `OpenAiServerConfig.Builder.port(int)`) where a fixed port matters, and point clients that were
+   configured for `http://localhost:8080` at the new port.
 5. **Platform floors:** Windows 10 or newer (Universal CRT), macOS 15.0, glibc 2.28 on Linux
    (RHEL 8, Ubuntu 20.04, Debian 10 and later), Android API 28; the ROCm jars need a ROCm 10
    runtime, the CUDA jars CUDA 13. 32-bit Windows and the `msvc-windows-x86-64` jar no longer ship.
@@ -50,8 +53,117 @@ the first two items break every existing consumer at upgrade.
 7. **The Java floor is still 8.** The C API (`jllama_api.h` in every natives directory), the
    provenance record (`jllama-build.txt`, `LlamaModel.getJllamaBuildInfo()`) and upstream's own
    `libllama` next to `libjllama` are additions; nothing else about loading or calling changed.
+8. **Values that were ignored or misread are refused when they are set** (`IllegalArgumentException`,
+   see Changed): `TrainingParameters` -- and with it both `LlamaTrainer.finetune` overloads -- checks
+   its documented bounds (`epochs` at least 1, `valSplit` in `[0, 1)`, exactly one of `trainingText` /
+   `trainingFile`, `modelPath` and `outputPath` set); `ModelParameters.setPredict` refuses values below
+   `-1` (`-2` stopped every request after one token; `0` behaves like `1`);
+   `addLoraScaledAdapter` / `addControlVectorScaled` refuse a path containing a colon, i.e. every
+   absolute Windows path, which llama.cpp's `FNAME:SCALE` format cannot express (load the adapter with
+   `addLoraAdapter` and set its scale with `LlamaModel.setLoraAdapter`, or use a relative path); an
+   empty or blank JSON fragment (`withJsonSchema("")`, `withToolsJson("")`, ...) and a `NaN` or
+   `+Infinity` token bias are refused instead of being sent as `null` or dropped; so are a
+   `ToolDefinition` parameter schema that is not exactly one JSON value (it was sent as a string,
+   which llama.cpp then refused without naming the tool) and an empty path for `addLoraAdapter` /
+   `addControlVector`.
+9. **The repeatable adapter options add up.** A second `addLoraAdapter`, `addLoraScaledAdapter`,
+   `addControlVector` or `addControlVectorScaled` call adds to the first instead of replacing it, as
+   their Javadoc always said; code that relied on the last call winning now loads every adapter.
+10. **One request, one prompt, one completion.** Several prompts in one request
+    (`"prompt": ["a", "b"]`) and an `"n"` other than 1 are refused as invalid requests
+    (`InvalidRequestException`, `400` in the servers) on every native path -- `LlamaModel.handle*` and
+    the streams, the C API, `llama-ffm` and every route of `OpenAiCompatServer` -- where the first prompt
+    and a single choice used to be answered without a word. Send separate requests; a token array is
+    still one prompt.
+11. **`ServerMetrics.getCumulativeUsage()` counts the cached prompt tokens**: `getPromptTokens()` is now
+    evaluated + cached tokens, like the `usage` of a response. Read `getProcessedPromptTokens()` (or
+    `getCumulativeProcessedPromptTokens()`) where the evaluated tokens alone were meant.
+12. **`LlamaModel.chatBatch` runs at most 64 requests at once** on threads of its own, and
+    `completeBatch` / `completeBatchWithStats` submit every request from the calling thread; neither
+    uses the common `ForkJoinPool` any more.
+13. **`OpenAiCompatServer`**: `--model-id` / `--model-alias` refuse a comma, an empty value and
+    leading or trailing whitespace, and a model file name with a comma is advertised with `_` in its
+    place; `/api/generate` without `"raw": true` applies the model's chat template, as Ollama does
+    (send `raw: true` for a bare completion); a route answers its own path only, so a sub-path such as
+    `/v1/messages/batches` is `404` (upstream's token-count routes are served, see Added); a negative
+    rerank `top_n` is `400`. Values the translations used to replace or drop without a word are
+    refused with `400`, as upstream or the protocol does (see Fixed): a `max_tokens` below `-1`, a
+    number field of the wrong type on every chat route (`"max_tokens": "100"` ran without a limit; a
+    boolean is refused too), `top_logprobs` without `logprobs: true`, `logprobs: true` on a streamed
+    request with tools, a `tool_choice` naming a tool the request does not carry or an object of a type
+    other than `function` (Anthropic: other than `auto`, `any`, `tool`, `none`), an Anthropic image
+    source other than `base64` or `url`, and on `/v1/responses` an input item of an unknown type, an
+    `input_image` without a URL and a `function_call` whose `arguments` is not a string; on
+    `/api/generate` a `prompt` that is not a string (it was answered as a preload, see Changed), a
+    `format` that is neither `"json"` nor a schema object (on `/api/chat` too), and without `raw` a
+    `template` or a `context`, which the chat template cannot apply. Anthropic's named
+    `tool_choice` (`{"type": "tool", "name": ...}`) now forces that one tool instead of allowing any.
+    A failure after a stream has started is reported as the route's error event (a `data:` error
+    object, an Ollama `{"error": ...}` line, Anthropic `event: error`, Responses `response.failed`)
+    and logged at WARN, where it ended the stream as if the answer were complete.
+14. **Infill fields and messages on the raw completion paths take effect or are refused.**
+    `complete` / `completeWithStats` / `generate` with `withInputPrefix` or `withInputSuffix` now
+    infill and need both fields and a model with FIM tokens; with `withMessages(...)` they render the
+    messages through the chat template, stop at the template's own stop strings, apply a reasoning
+    budget through the template's thinking tags, and refuse a non-empty prompt, the infill fields or
+    a chat-only field (`response_format`, `tools`, `tool_choice`, `parallel_tool_calls`,
+    `top_logprobs`, `thinking_budget_tokens`) next to them (see Fixed). All of them used to be
+    dropped without a word. Without messages, a reasoning budget needs its tags from the request
+    (`withReasoningBudgetStartTag` / `withReasoningBudgetEndTags`, see Added).
+15. **Slot files are file names inside `--slot-save-path`.** `LlamaModel.saveSlot` / `restoreSlot`,
+    `Session.save` / `restore` / `checkpoint` / `fork`, the C API's `jllama_slot_action` and
+    `llama-ffm` take a file name, which the library resolves inside the directory
+    `ModelParameters.setSlotSavePath(...)` names, as llama.cpp's own slot route does; up to 5.1 the
+    argument was the path itself and the directory had no effect. Without the directory saving and
+    restoring are a `LlamaException`, and a name with a directory part, `..` or nothing is an
+    `InvalidRequestException`; `eraseSlot` needs neither. `SessionCheckpoint.getFilepath()` is
+    `getFilename()`. Load with `setSlotSavePath(dir)` and pass `"slot0.bin"` where you passed
+    `dir + "/slot0.bin"`. Slot files written by 5.1 do not restore at all (llama.cpp's state format
+    moved on, see Changed); regenerate them.
+16. **`LlamaLoader.initialize()` loads once per JVM.** A second call after a successful load returns
+    without searching again; a `net.ladenthin.llama.lib.path` or `net.ladenthin.llama.backend` changed
+    in between takes effect only in a new JVM, which the loader says once on stderr. It used to load a
+    second copy of the library and replace the build info, although the JVM keeps using the first.
+17. **A malformed stream envelope is an error, not the end of the stream.** `ChatStreamChunkParser.feed`
+    throws `ChatStreamChunkParser.MalformedEnvelopeException` (an `IllegalArgumentException`) for an
+    envelope that is not `{"data": , "stop": }` -- an empty one included,
+    which used to end the stream; through `LlamaModel`'s streams it is a `LlamaException` (`500` in
+    the server).
+18. **`llama-langchain4j` streams tool calls in a different order.** `onCompleteToolCall` of a call
+    fires when the next call starts, not at the end of the stream, and arguments that arrived before
+    a call's name are reported through `onPartialToolCall` once the name is known, so the partial calls
+    add up to the complete arguments; a fragment that would change a call already reported complete
+    fails the stream through `onError`.
 
 ### Added
+- **llama.cpp b11815** (from b11639, in eight steps on the branch: b11760, b11761, b11799, b11800, b11803, b11809, b11814, b11815). What the
+  range brings to a consumer: `POST /tts` (llama.cpp b11781, #26603, speech from a TTS-capable model through the
+  embedded server) and the realtime transcription WebSocket `/v1/realtime` (b11780, #30636), both served by
+  `NativeServer` -- classic and attach mode -- and not by the Java transport or `LlamaModel` yet; a
+  TTS-capable model lists `"audio"` in `ModelMeta.getOutputModalities()`; `HTTP_PROXY` / `HTTPS_PROXY` /
+  `NO_PROXY` honoured by the model download (b11699, #21752); logprobs with speculative decoding (b11776);
+  Kolibri-1 as upstream's own architecture (b11709, see its entry); the NEON fp32->fp16 selection upstream
+  (b11761, this project's #30505); two architectures from the last step, Flint (`FlintForCausalLM`, b11803,
+  #30928) and SenseNova U1 (text plus its vision projector in mtmd, #28919), and a jinja fix (#30941: a
+  `namespace` attribute is no longer shadowed by a dict method of the same name, so `ns.items` is the
+  attribute again, as in jinja2). The b11809 step (2026-10-22): LoRA adapters on prism.hadamard
+  folded matmul weights load now (#30910; an adapter on a latent lookup table such as `token_embd` is
+  still refused), dflash reads an optional `fc.input_scale`, OpenCL gets an F32-to-I32 ARGMAX kernel
+  (#30897, embedded in the three OpenCL module jars) and WebGPU Q2_0 -- nothing here changed. The b11814
+  step (2026-10-22): `tokenizer.ggml.clean_spaces` is a GGUF key (#30940; the converter writes it from
+  `tokenizer_config.json`, and the pre-tokenizer defaults turn the clean-up of spaces before punctuation
+  off for `lfm2`, `glm4`/`glm5`, `dbrx`, `exaone4`, `mellum2`, `minicpm5`, `gemma-4`,
+  `granite-embed-multi-311m` and `chatglm-bpe` models, whose detokenized text keeps that space now), Pixtral GGUFs tokenize with the Tekken
+  pre-tokenizer (#30960), KleidiAI v1.32.0 with SVE Q8_0 kernels (#30808, not built here) -- nothing
+  here changed. The b11815 step (2026-10-22, one commit): the SYCL `CONV_2D` kernel accepts the
+  channel-fastest CWHN layout instead of aborting (#30363), which reaches the two SYCL module jars.
+  Two patches dropped (`0016`, `0019`), `0007` refreshed.
+- **`RouterModel.Status.INCOMPLETE`** (llama.cpp b11714): a router download that never finished is listed
+  as `"incomplete"` without a worker; it was `UNKNOWN` here.
+- **`/v1/messages` passes `id_slot` through and merges `extra_params`** like upstream (llama.cpp b11756 and
+  b11795, #30856): llama.cpp's own sampling parameters (`seed`, `min_p`, `repeat_penalty`, ...) travel in an
+  `extra_params` object and win over a translated key; the two keys the translation reserves for its own
+  answer (`verbose`, `response_fields`) stay its own.
 - **The C API of `libjllama` (`jllama_api.h`, shipped in every natives directory).** Every operation
   of `LlamaModel`, `LlamaQuantizer`, `NativeServer` (classic, attach and the router's worker
   command), `RpcServer`, `TextToSpeech` and `LlamaTrainer` is a plain C function with an opaque
@@ -203,12 +315,14 @@ the first two items break every existing consumer at upgrade.
 - **`ModelParameters.setMoeCacheMib(int)`** (`--moe-cache-mib`, llama.cpp b11480, EXPERIMENTAL upstream): a
   GPU cache for the MoE expert weights a `setCpuMoeLayers` / `--cpu-moe` setup keeps in host memory,
   split among several GPUs like the layers; `0` (the default) disables it.
-- **Kolibri-1 support** (Aleph Alpha, architecture `kolibri1`, 78B German/English reasoning MoE) ahead of upstream
-  llama.cpp ([ggml-org/llama.cpp#29922](https://github.com/ggml-org/llama.cpp/issues/29922)), as the carried patch
-  `0016-model-kolibri1.patch`. It combines the two community ports and, unlike either of them, loads the GGUFs of
-  both community converters. Guarded by `test_kolibri1.cpp`, which compares tiny random models with an
-  independent reference written from Aleph Alpha's vLLM implementation. The patch is dropped once upstream adds
-  the architecture.
+- **Kolibri-1 support** (Aleph Alpha, architecture `kolibri1`, 78B German/English reasoning MoE): upstream's own
+  implementation since llama.cpp b11709 ([ggml-org/llama.cpp#30673](https://github.com/ggml-org/llama.cpp/pull/30673)),
+  which this library carries from the b11760 bump on; before it the patch `0016-model-kolibri1.patch` (dropped at
+  that bump, see `docs/history/dropped-llama-patches.md`). The patch also loaded the GGUFs of the two community
+  converters; upstream does not -- it requires the keys its own converter writes and knows no pre-tokenizer
+  `kolibri1` -- so those files must be reconverted with upstream's `convert_hf_to_gguf.py` (b11709 or later).
+  Guarded by `test_kolibri1.cpp`, which compares tiny random models with an independent reference written from
+  Aleph Alpha's vLLM implementation, now against upstream's code.
 - **`GpuSplitMode.TENSOR`** (`--split-mode tensor`, tensor parallelism, EXPERIMENTAL upstream). The mode
   existed upstream before; since llama.cpp b11450 (#26610) it also works across RPC servers.
 - **Input/output modalities on `RouterModel` and `ModelMeta`** (llama.cpp b11429, #29987):
@@ -236,8 +350,89 @@ the first two items break every existing consumer at upgrade.
   starts it with the CPU natives of every desktop platform, no checkout and no download by hand. The
   release-asset jar without the core stays on the GitHub release. The agent's version now moves with the
   core's; `check-natives.py` fails when they differ.
+- **`InferenceParameters.withLogitBiasJson(String)`**: `logit_bias` as one checked JSON value in either
+  form llama.cpp's request schema reads -- an array of `[token, bias]` pairs or OpenAI's object of
+  token ids to biases, `false` banning a token. The OpenAI request mapper forwards a client's
+  `logit_bias` with it, and `n_probs` with `withNProbs`, on every chat route.
+- **`InferenceParameters.withResponseFields(String...)`** (request field `response_fields`,
+  `RequestField.RESPONSE_FIELDS`): upstream's projection of the native answer, and with `withVerbose`
+  of the `__verbose` object of an OpenAI answer, onto slash-separated paths (`timings/predicted_n`);
+  the projected key is the whole path, and a path the result lacks is dropped without an error, as
+  upstream does. The OpenAI mapper forwards a client's `response_fields` and answers `400` for
+  anything but an array of strings.
+- **`RouterModel.getAliases()` and `isNamed(String)`**: the aliases `GET /models` lists for a model
+  (`--alias` of a preset), parsed by `RouterModelsResponseParser`; `RouterClient.findModel` and
+  `awaitModelLoaded` match a model by its id or any alias, as the router resolves the name of a
+  request.
+- **`OpenAiServerConfig.Builder.totalSlots(int)` / `getTotalSlots()`** (advertised as `total_slots` in
+  `GET /props`) and the `OpenAiServerCli.Options.toServerConfig` overloads taking the context size
+  of one slot and the slot count of the loaded model, which `OpenAiCompatServer.main` now passes.
+- **`RerankResponseParser.toDocumentScores(...)`**: the document-to-score map of a rerank result in
+  the order of the documents (see Fixed).
+- **`ModelParameters.enableContextShift()`** (`--context-shift`, `ModelFlag.CONTEXT_SHIFT`): llama.cpp
+  has context shifting off by default, so a slot stops when its context is full; this turns it on.
+  `disableContextShift()` said the default was "enabled"; its Javadoc now names upstream's default, and
+  each of the two methods replaces an earlier call of the other, so the later call wins.
+- **`OpenAiCompatServer` serves upstream's token-count routes**: `POST /v1/messages/count_tokens`
+  (Anthropic, `{"input_tokens": N}`; Claude Code asks there), `/v1/responses/input_tokens` and
+  `/v1/chat/completions/input_tokens` (`"object": "response.input_tokens"`), each also without the
+  `/v1` prefix. The body is translated as on the generating route, rendered by the model's chat
+  template and tokenized as llama.cpp counts it; a refused translation is `400` in the route's error
+  form. Content that only the multimodal projector can count (image, audio, video) is `501` naming
+  the part, where upstream counts it -- a count that left it out would be too low. The new default
+  method `OpenAiBackend.countTokens(JsonNode)` (`501` unless overridden) carries it;
+  `LlamaModelBackend` implements it.
+- **The reasoning budget's tags in the builder**: `InferenceParameters.withReasoningBudgetStartTag`,
+  `withReasoningBudgetEndTags` and `withReasoningBudgetMessage` (`reasoning_budget_start_tag`,
+  `reasoning_budget_end_tags`, `reasoning_budget_message` of llama.cpp's completion schema), so
+  `withReasoningBudgetTokens` takes effect on `complete()` / `generate()` without messages too. An
+  empty tag or no end tag is an `IllegalArgumentException`; with messages, and on the chat paths, the
+  template's own thinking tags win where it has some. The budget starts counting at a start tag the
+  model generates (one at the end of the prompt is not seen, as upstream; use messages there), and a
+  tag the vocabulary holds as a special token shows as text only with `preserved_tokens`.
+  `reasoning_control` gets no builder method: it arms the sampler for llama.cpp's runtime
+  `reasoning_end` action, which this library does not offer.
+- **Images on the raw completion paths.** The raw completion requests over JSON
+  (`LlamaModel.handleCompletions` / `handleCompletionsOai` / `streamCompletionsOai`), the C API and
+  `llama-ffm` take upstream's multimodal prompt object, `"prompt": {"prompt_string": "... 
+  ...", "multimodal_data": [""]}`, on a model loaded with `--mmproj`; it was refused as if
+  the model had no projector. `ModelMeta.getMediaMarker()` (`media_marker` of the model meta, as
+  upstream's `/props`) names the marker, which llama.cpp draws per process unless
+  `LLAMA_MEDIA_MARKER` pins it. Markers that do not match the images, data that is not base64 and a
+  prompt that ends on the image are invalid requests -- the last one aborted the process inside
+  llama.cpp. The default method `OpenAiBackend.mediaMarker()` passes the marker to the Ollama
+  translation.
 
 ### Changed
+- **README corrected against the code** (an audit of every section, 63 findings): no 32-bit Windows
+  natives; the snapshot channel is a manual Publish run, not every push; OpenVINO is opt-in wherever
+  the loader's additive rule is described; the static library jars (macOS, s390x) and the single
+  Windows-arm64 CPU module; the release assets (AARs, the agent jar, the signed source archive); the
+  build options the configure overrides; GPU backends are module builds, not a `-DGGML_CUDA=ON` of
+  the library, and `-DLLAMA_CURL` is gone; the loader's search order and the four Windows extras; the
+  `jllama-files.txt` rule when a module is added to a natives directory; `handleTokenize` /
+  `handleDetokenize` take plain arguments; `/v1/completions` streams; `OpenAiServerConfig.builder()`
+  and the `x-api-key` header; `--rpc` in the CLI list; the `INCOMPLETE` / `UNKNOWN` router statuses;
+  RPC ships in the library jars; the LangChain4j streaming adapter and its mapped features; the agent's
+  prompt and status line; `chatWithTools` stops at `withMaxToolRounds`; the infill prompt position;
+  the mmproj device-offload table; video input takes `data:` URIs and URLs; the reactive sections'
+  cancellation semantics (a reader released cancels the generation; an unclosed one runs to its natural
+  stop), the Kotlin wrappers' cancellation, the Akka snippet; the logging verbosity split; the Android
+  AAR's ABIs, the emulator coverage, the from-source recipe (paths, Gradle 9, dependencies, ProGuard);
+  the Windows import table; the PIT scope; three test properties added to the reference table.
+- **The SYCL natives jobs install upstream's toolchain**, as llama.cpp's `ubuntu-24-sycl` / `windows-sycl`
+  release jobs do: Intel's Deep Learning Essentials 2026.1.4 and oneDNN 2026.0.2 offline installers (upstream
+  since b11654, #30131), pinned by URL, instead of the unpinned apt packages on Linux and the 2025.3.3 bundle on
+  Windows; and the Level Zero loader/SDK upstream's jobs have had since #21597 / #29273. **Two consequences
+  for the shipped modules:** both now import the Level Zero loader (`libze_loader.so.1`, `ze_loader.dll`; the
+  Intel GPU driver's) and carry ggml-sycl's Level Zero allocation path (`GGML_SYCL_SUPPORT_LEVEL_ZERO_API`),
+  which the modules built so far never had -- their import lists carried no loader; and the Windows module
+  needs the oneAPI **2026** runtime (`sycl9.dll`, `mkl_sycl_blas.6.dll`) where the 2025.3 build needed
+  `sycl8.dll` / `mkl_sycl_blas.5.dll` -- with the older runtime on `PATH` the module no longer loads, and
+  ggml skips it silently (the CPU modules run). README names the runtime per jar.
+- `TextToSpeech` follows llama.cpp b11796 (#30902): `mtmd_helper::gen_audio` is initialised explicitly and
+  the synthesis input starts from upstream's defaults (a `stream` flag joined the struct, off here). No
+  change to the Java API; a projector without audio generation is refused with a message, as before.
 - Dependencies and build tooling bumped in step with the sibling repositories: Jackson 2.22.3 ->
   2.22.4 (`jackson.version`, which the `jackson-bom` and the AAR pom follow), LogCaptor 2.12.7 ->
   2.13.0 (test; still requests log4j 2.26.1, so the log4j floor guard is unchanged); in the agent
@@ -245,6 +440,76 @@ the first two items break every existing consumer at upgrade.
   already had it); jsign 7.2 -> 7.6 for the Windows code signing (`sign-windows-natives.sh`, new
   SHA-256; same options, same eSigner authentication path); Gradle 9.8.1 -> 9.8.2 (`GRADLE_VERSION`
   and the shared `verify-signing-key-gradle` job); `anthropics/claude-code-action` v1.0.255.
+- **Values that were ignored, misread or left to fail at load time are refused when they are set,
+  with an `IllegalArgumentException` naming the value:**
+  - `TrainingParameters.toJson()` (and so `LlamaTrainer.finetune`) checks the documented bounds before
+    the JNI call: `epochs(0)` ran no epoch and wrote the unchanged model to `outputPath` as a silent
+    success, both corpus fields set let `trainingFile` win, a `valSplit` outside `[0, 1)` (`NaN`
+    included) reached ggml unchecked, and a missing `modelPath` / `outputPath` was a bare
+    `NullPointerException`.
+  - `ModelParameters.setPredict` refuses values below `-1`. Its Javadoc promised `-2` = "until
+    context filled", which is a setting of llama.cpp's completion tool: in the server a slot counts
+    only `-1` as unlimited, so every request stopped after its first token without an error
+    (measured). The Javadoc of `setPredict` and `InferenceParameters.withNPredict` now says that `0`
+    behaves like `1`, and that the server answers a per-request `n_predict` of `-2` as an invalid
+    request.
+  - `setRopeScaling(LONGROPE)` and `setRopeScaling(MAX_VALUE)` throw, where they made the model
+    unloadable (llama.cpp's parser takes only `none`, `linear` and `yarn`); `UNSPECIFIED` removes the
+    option and leaves the model's default, as `setPoolingType(UNSPECIFIED)` now does too (it left an
+    earlier `--pooling` in place).
+  - A token bias of `NaN` or `+Infinity` (`withTokenIdBias`, `withTokenBias`) is refused, and
+    `-Infinity` bans the token: Jackson wrote non-finite floats as strings, which llama.cpp skips, so
+    banning a token that way had no effect.
+  - `JsonParameters` refuses an empty or blank fragment (`withJsonSchema("")`,
+    `withResponseFormat("")`, `withToolsJson("")`, ...), which the strict reader parsed to nothing and
+    the request carried as `"key": null`, i.e. generation ran without the schema the caller set.
+  - `ModelParameters.addLoraAdapter("")` and `addControlVector("")` are refused ("`--lora` needs a
+    non-empty file path"): an empty path was silently overwritten by the next call, or reached
+    llama.cpp, which failed the load with a message that did not name the option.
+  - `OpenAiServerConfig.Builder.heartbeatMillis` refuses a value of `0` or below: as the period of the
+    heartbeat timer it made every streamed request fail with `200` and an
+    `IllegalArgumentException` error event, reported as the client's fault.
+  - `OpenAiServerCli` refuses a `--port` outside 0..65535 as a usage error before the model loads;
+    `OpenAiCompatServer.main` used to load the model, which can take minutes, and then die with an
+    uncaught "port out of range".
+- **`LlamaModel.completeBatch`, `completeBatchWithStats` and `chatBatch` no longer run on the common
+  `ForkJoinPool`**, whose `availableProcessors() - 1` threads the application shares: 8 requests on
+  8 slots ran 3 at a time on a 4-core machine (measured). `completeBatch` and
+  `completeBatchWithStats` submit every request from the calling thread and then receive the results
+  in order, releasing the requests already submitted when a submission fails; `chatBatch`, whose
+  native call submits and waits in one, runs each request on a daemon thread of its own, at most 64
+  at once, and throws the first failure of any kind only after every request has finished (an
+  `Error` used to end the wait while other requests still ran). In all three, an `Error` -- an
+  `OutOfMemoryError` while a large request is rendered or its result read -- no longer skips the
+  release of the requests already submitted or the receiving of the others, and every failure after
+  the first is attached to it as a suppressed exception instead of being dropped.
+- **`llama-langchain4j` reports a streamed tool call complete when the next one starts.**
+  `onCompleteToolCall` of every call came only at the end of the stream, after the partial calls of
+  the following ones; it now fires for the earlier calls as soon as a call with a higher index
+  arrives, as langchain4j's own OpenAI integration does, so a handler can run a tool while the model
+  still streams the next. A call that has no name yet stays open until the stream ends, and a
+  fragment that would change a call already reported complete fails the stream through `onError`
+  (an `IllegalStateException` naming the index) instead of silently diverging from the reported call.
+- **Ollama `/api/generate` follows Ollama's semantics.** It completed every prompt raw; now a request
+  with `suffix` is fill-in-the-middle as before, one with `"raw": true` a bare completion (a `system`,
+  `template` or `context` next to it is refused with 400, as Ollama does), and any other goes through
+  the model's chat template with `system` as the system message and the prompt's `images`, `format`
+  and `think` honoured, answering `thinking` next to `response`. A request with neither a prompt nor
+  a suffix -- how clients preload a model -- is answered at once with Ollama's load response
+  (`done: true`, empty `response`, `done_reason` `"load"`; a single object even when streamed)
+  instead of answering an empty user turn. A `prompt` that is not a string (an array, an object, a
+  number) is refused with `400` and Ollama's error body, as Ollama does when it binds the request,
+  before any of these branches; it was taken for a preload or for an empty prompt. Three deliberate
+  differences from Ollama remain: a request with a suffix but no prompt stays fill-in-the-middle at
+  the start of the file (Ollama answers it with the load response), an empty `"suffix": ""` counts as
+  a suffix, and `keep_alive: 0` does not unload the model and is answered `"load"` (Ollama unloads
+  and answers `"unload"`). `/api/chat`
+  without messages is answered the same way, as Ollama preloads with it; it was refused with `400`.
+- **`OpenAiCompatServer --model-id` / `--model-alias` take a value llama.cpp keeps as one alias.**
+  llama.cpp splits `--alias` at commas and strips every part, so `--model-id my,model` was advertised
+  as `my,model` in `/v1/models` and named `model` in every answer, and a blank id fell back to the
+  model path. A comma, an empty value and leading or trailing whitespace are a usage error now; an id
+  derived from a file name replaces a comma with `_` and is trimmed (blank: the default id).
 - **`libjllama` exports only its interface.** Hidden visibility for the project's own translation
   units, a version script on ELF and an exported-symbols list on macOS keep the statically linked
   llama, llama-common, mtmd, cpp-httplib and BoringSSL symbols local: the Linux library exported
@@ -262,8 +527,16 @@ the first two items break every existing consumer at upgrade.
   `sign-windows-natives` (`.github/sign-windows-natives.sh`: jsign, the secrets of the `maven-central`
   environment, an RFC 3161 timestamp) re-uploads the Windows natives artifacts signed, `signed=yes`
   in each directory's `jllama-build.txt`, and `verify-native-deps.py` refuses an unsigned DLL on such a
-  run. Without the switch every build says `signed=no`, as before. (Azure Trusted Signing, the first
-  choice, takes no individual in Germany and was dropped before it ever ran.)
+  run. Without the switch every build says `signed=no`, as before, and the job runs as a no-op in
+  the unprotected environment `unsigned` rather than being skipped: a skipped job skips every job
+  behind `package` with it (GitHub's dependency-chain rule), which is what the first form did -- the
+  smoke sets, the report and both publish jobs of every run from 2026-10-16 to 2026-10-22 were
+  skipped behind a green `package`, with the run concluding "success". (Azure Trusted Signing, the
+  first choice, takes no individual in Germany and was dropped before it ever ran.) The first run to
+  reach `package`'s natives-jar load step after that (39763815465) died in `smoke-natives-jars.sh` on
+  `path: unbound variable`: the F269 commit had replaced the script's classpath line with the
+  Main-Class check, and `bash -n` cannot see an unbound variable. The line is back, proven with a
+  stub `java` that records the classpath it is given.
 - **A `v*` tag release is built without the shared compiler cache.** `USE_CACHE` is `false` on every
   tag ref whatever the dispatch input says, so each natives build of a release compiles every object
   in the job that ships it; snapshots and PR builds keep the sccache/Depot cache. The cache provider
@@ -386,9 +659,25 @@ the first two items break every existing consumer at upgrade.
   other `LlamaException`.
 - **`NativeServer` binds port 9931 when no `--port` is given** (llama.cpp b11521,
   [#30159](https://github.com/ggml-org/llama.cpp/pull/30159) moved the server default off 8080), in classic and
-  attach mode alike; `NativeServer.getPort()` reports 9931 accordingly. `OpenAiCompatServer` keeps its own default
-  of 8080 (`OpenAiServerConfig.DEFAULT_PORT`, a public constant of this project with no upstream coupling). Pass
-  `--port` explicitly where a fixed port matters; every README example already does.
+  attach mode alike; `NativeServer.getPort()` reports 9931 accordingly. **`OpenAiCompatServer` follows**:
+  `OpenAiServerConfig.DEFAULT_PORT` and the `--port` default of its command line are 9931 instead of 8080,
+  so both servers of the library default to llama.cpp's port; the agent's documentation, the CLI help and
+  the Javadoc examples name it. Pass `--port` explicitly where a fixed port matters; every README example
+  already does.
+- **Slot files are resolved inside `--slot-save-path`, as llama.cpp's slot route does.**
+  `LlamaModel.saveSlot` / `restoreSlot`, `Session.save` / `restore` / `checkpoint` / `fork`, the C API's
+  `jllama_slot_action` and `llama-ffm` used their argument as the path itself, so
+  `ModelParameters.setSlotSavePath(...)` had no effect, `../x.bin` or an absolute path was written
+  wherever it pointed, and a model without the directory saved anyway. The argument is a file name now:
+  without the directory, saving and restoring are a `LlamaException` naming the option; a name with a
+  directory part, `..` or nothing is an `InvalidRequestException` (`JLLAMA_STATUS_INVALID_REQUEST`);
+  erasing a slot needs no directory. The parameters are called `filename`, and
+  `SessionCheckpoint.getFilepath()` is `getFilename()`.
+- **`LlamaLoader.initialize()` is idempotent after the first successful load.** Every call searched
+  again (only the nested call from `JNI_OnLoad` returned early), so a call after `lib.path` or the
+  backend filter had changed loaded a second copy of the library, or extracted a second module set, and
+  replaced the build info while the JVM kept using the first library. A further call now loads nothing
+  and names a property changed since the load once on stderr; a failed attempt is retried.
 - **CI: the Linux CUDA build no longer routes nvcc through sccache by default** (`SCCACHE_WRAP_NVCC=true`
   in `build.sh` opts back in). With CUDA 13.4 and sccache 0.18.0 the nvcc launcher failed on the first `.cu`
   files in two confirmed runs (`Missing "cubin" file output`, `Compiler killed by signal 126`), and the
@@ -633,6 +922,427 @@ the first two items break every existing consumer at upgrade.
   changed meaning accordingly (see Added).
 
 ### Fixed
+- **`jllama_rpc_server_stop()` (C API, `llama-ffm`) is for the serve calls in progress when it is made.**
+  It waited for the count of serves to reach zero and reset the stop request only then, so a serve
+  that took over right after the one it targeted had ended (a restart from another thread, which
+  `FfmRpcServer` allows) was ended too: it returned without binding, or lost its listener once bound.
+  Every serve call has an id now; the stop records the newest as its target, waits for the serves up
+  to it, and issues ggml's stop under the mutex a new serve's registration takes first (F271). The
+  Java `RpcServer` was not affected: its single-instance slot stays taken until `close()` has returned.
+- **The classes jar carries `Main-Class: net.ladenthin.llama.server.ServerLauncher` again.** The entry
+  had gone with the fat-jar assembly, so `java -jar llama-.jar` and the README's
+  `jbang --deps  net.ladenthin:llama:` lines, which run a GAV through its manifest,
+  had no entry point ("no main class deduced") although the README, CLAUDE.md and the launcher class
+  all described one. The default jar execution names it now (the natives jars keep their
+  Automatic-Module-Name manifests), and `smoke-natives-jars.sh` reads it back from the built jar in the
+  `package` job (F269).
+- **`OpenAiCompatServer`: what every surface shares.**
+  - **`--api-key` accepts Anthropic clients.** Only `Authorization: Bearer ` with a capital
+    `Bearer` passed, so every Anthropic SDK -- which sends `x-api-key` -- got `401` on `/v1/messages`
+    once a key was set; `x-api-key` is accepted now and the scheme compared case-insensitively
+    (RFC 7235), both still in constant time.
+  - **The CORS preflight allows the headers the browser asks for.** With `corsAllowOrigin` set it
+    always answered `Access-Control-Allow-Headers: Content-Type, Authorization`, so a browser dropped
+    the requests of the Anthropic SDK (`x-api-key`, `anthropic-version`, `x-stainless-*`). It mirrors
+    `Access-Control-Request-Headers` now (the origin stays the gate), and without one allows
+    `Content-Type, Authorization, x-api-key, anthropic-version`.
+  - **A route answers its own path only.** `com.sun.net.httpserver` dispatches by longest prefix, so
+    `POST /v1/messages/count_tokens` (Claude Code counts tokens there) ran a full generation and
+    `/v1/modelsXYZ` listed the models; every other path is `404` now, as upstream, and `/v1/reranking`
+    is registered as the alias of `/v1/rerank` it only was by prefix. The token-count routes are
+    served since (see Added).
+  - **The advertised context matches what a request can use** (`maxInputTokens` / `maxOutputTokens`,
+    the context length of `/api/show` and `/props`, which Copilot & co. size their prompts by). The
+    input budget was `-c` minus 2048 whatever the output budget was (`-c 2048` advertised 1 input
+    token); with `--parallel N` the whole `-c` was advertised although each slot holds `-c / N`; and
+    without `-c` the fixed defaults 8192 + 2048 were advertised whatever the model loaded with (10240
+    for slots of 2048, measured). The server now advertises the context of one slot of the loaded
+    model, else `-c` divided by an explicit `--parallel`, and splits it between input and output.
+  - **`/props` reports the slot count** of the loaded model (else `--parallel`, else 1) as
+    `total_slots`, where it was always 1.
+  - **`/v1/models` carries `created`**, one of the four required fields of OpenAI's model object;
+    strictly typed clients (Rust's `async-openai`, schema validators) could not read the list.
+  - **A heartbeat no longer swallows the opening event of a stream.** The heartbeat timer was
+    scheduled before the opening of `/v1/messages` and `/v1/responses` was set, so a heartbeat that
+    fired first started the stream without `message_start` / `response.created`, which Anthropic SDKs
+    reject. The opening is now always the first event.
+- **`OpenAiCompatServer`: the chat request mapping**, which every chat request goes through -- OpenAI
+  directly and the Ollama, Anthropic and Responses routes after their translation:
+  - **The reasoning controls reach the model.** `chat_template_kwargs` (Qwen3's `enable_thinking`),
+    `reasoning_effort` (`"none"` switches thinking off through `enable_thinking: false`, another value
+    becomes the template variable `reasoning_effort`) and `reasoning_budget_tokens` /
+    `thinking_budget_tokens` were dropped; they follow the rules of upstream's chat parser now.
+  - **llama.cpp's own sampling fields are forwarded**: `min_p`, `typical_p`, `repeat_penalty`,
+    `repeat_last_n`, `top_n_sigma`, `xtc_*`, `dynatemp_*`, `min_keep`, `n_keep`, `ignore_eos`,
+    `grammar` and `json_schema` (both together are refused with upstream's `400`, as llama-server
+    does; the schema used to be dropped next to a grammar), and `n_predict`, which wins over
+    `max_tokens` / `max_completion_tokens` as upstream; so are `logit_bias` and `n_probs`.
+  - **`logprobs` is read as upstream's chat parser reads it**: `logprobs: true` becomes `n_probs`
+    (`top_logprobs`, else 20), a number is `n_probs` itself (`"logprobs": 2` asked for 20 and
+    overrode an `n_probs` next to it), and `top_logprobs` without `logprobs: true` is `400` with
+    upstream's message, as is a `logprobs` that is neither a boolean nor a number; both were dropped
+    or misread without a word.
+  - **A number field of the wrong type is `400`**, as upstream's schema answers it (`Field 'max_tokens':
+    type must be number, but is string`), on every chat route: the mapper read only numbers and left
+    everything else out, so `"max_tokens": "100"` ran without a limit and a decimal `n_probs` or
+    `repeat_last_n` was dropped. `null` still means "not set" and a decimal in an integer field is cut
+    as upstream does; a boolean is refused, where llama.cpp would read it as 0 or 1. The Anthropic
+    (`max_tokens`, `temperature`, `top_p`, `top_k`), Responses (`temperature`, `top_p`,
+    `max_output_tokens`) and Ollama translations (every number option) read their numbers the same way.
+  - **`logprobs: true` on a streamed request with tools is `400`** with upstream's message
+    (`logprobs is not supported with tools + stream`); the mapper turned it into `n_probs`, so the
+    refusal was unreachable. A number `logprobs`, a blocking request and one without tools stay allowed.
+  - **`id_slot: -1` means any slot**, upstream's default, instead of a `400`; and `max_tokens: 0` is
+    passed on as upstream reads it instead of meaning "no limit". A `max_tokens` (or `n_predict`,
+    `max_completion_tokens`) below `-1` is answered with upstream's `400` and an `n` other than 1 is
+    `400`; both ran with the defaults (no limit, one answer) without a word.
+  - **A `tool_choice` naming a function** (`{"type": "function", ...}`, also the Responses form) became
+    `auto`, so the model could answer without calling it, or any other tool. Only the named tool is
+    passed on now, with `required`, so the model must call exactly that one (llama.cpp knows no
+    named choice; the template therefore no longer shows the model the other tools). A name no tool
+    of the request carries, and a `function` choice without a name, are `400`. A `tool_choice` object
+    of another type (`allowed_tools`, `custom`, `mcp`, a hosted tool) ran as `auto`, so the requested
+    restriction vanished; it is `400` naming the type, here and on `/v1/responses`.
+- **Ollama routes (`/api/*`).**
+  - **Images reach the model.** `/api/chat` ignored a message's `images`, although `/api/show`
+    advertises `vision` with `--mmproj`; they become `image_url` parts (the MIME type from the magic
+    bytes).
+  - **The second turn of a conversation with tools works.** Ollama sends earlier tool calls without
+    `type`, which llama.cpp's parser requires, so every request carrying one was refused with `400`;
+    `"function"` is set now. A tool result's `tool_name` becomes the `name` llama.cpp reads, and a
+    tool call without arguments is answered with `"arguments": {}` instead of `null`.
+  - **`think` works**: `false` switches thinking off, `true` switches it on even on a server started with
+    thinking disabled (as `enable_thinking: true` does on the OpenAI route), a level string (`"low"`,
+    ...) is passed as `reasoning_effort`. The thinking itself is answered as `message.thinking`, blocking
+    and per stream line, and `thinking` in the history goes back to the model as `reasoning_content`;
+    both were lost.
+  - **The sampling options are forwarded on all three routes** (`/api/chat`, `/api/generate`, and
+    fill-in-the-middle with `suffix`, which took only `temperature` and `num_predict`):
+    `temperature`, `top_p`, `top_k`, `seed`, `stop`, `repeat_penalty`, `repeat_last_n`, `min_p`,
+    `typical_p`, `presence_penalty`, `frequency_penalty` and `num_keep` (as `n_keep`). Ollama's
+    `num_predict: -2` ("fill the context") is passed as `-1` instead of being refused, and
+    `repeat_last_n: -1` is left to the server default.
+  - **`done_reason` and the token counts are right.** `done_reason` is `length` for an answer cut off
+    by `num_predict` and `stop` otherwise on all four ways (the blocking chat passed OpenAI's
+    `tool_calls` through, the others always said `stop`); the streamed `/api/chat` done line and both
+    `/api/generate` forms report `prompt_eval_count` / `eval_count`.
+  - **`/api/version` reports `0.9.0`.** VS Code Copilot's Ollama provider refuses a server below
+    0.6.4, and the route said `0.1.0`, so the whole Copilot path failed at that check.
+  - **`/api/show` names the context length where Continue reads it**: `parameters` carries
+    `num_ctx `.
+  - **`/api/generate` with `raw: true` keeps `format` and `images`**, as Ollama does: `format` becomes a
+    JSON schema (`"json"` an object schema) on the raw and the fill-in-the-middle way, and each
+    `[img-N]` marker of the raw prompt is replaced by the server's media marker, image `N` passed as
+    multimodal data; both were dropped. A `format` that is neither `"json"` nor a schema object is
+    `400` on every way (`/api/chat` included), as Ollama answers it, and so are a marker without its
+    image, an image without its marker (Ollama drops it) and an image beside `suffix`.
+  - **A backend that takes no images answers a raw `/api/generate` with images `501`, not `500`.** The
+    default `OpenAiBackend.mediaMarker()` throws `UnsupportedOperationException`, which the route took
+    for a server fault; it is upstream's answer for a capability the server lacks now, with the
+    exception's message in Ollama's error shape. `LlamaModelBackend` is not affected.
+  - **`/api/generate` without `raw` refuses `template` and `context`** with `400`: the model's chat
+    template renders the prompt and token contexts cannot be replayed, so both were ignored and the
+    request was answered as an ordinary chat turn. Empty values count as not set, as in Ollama.
+  - **A malformed `/api/chat` is answered with `400`.** A `messages` element that is not an object
+    (a string, `null`, an array) ended the request with a closed connection and no status line, and
+    a `messages` that is not an array was read as "no messages"; both are refused with upstream's
+    message and the element's index now.
+- **Anthropic Messages (`/v1/messages`).**
+  - **Images reach the model**: `image` blocks of a message (`base64` or `url` source) and of a
+    `tool_result` become `image_url` parts; both were dropped. A `base64` image without `media_type`
+    is named by its bytes (PNG, JPEG, GIF, WebP), else `image/jpeg` as upstream; it was always
+    `image/png`. An image source of another type -- the Files API's `file`, which a local server cannot
+    fetch -- and a `base64` or `url` source without its string are `400` naming the problem; upstream
+    drops such a block, and the message went on as text without the image.
+  - **Several text blocks of one message are joined with a newline**, as upstream does for a template
+    that takes text only; `first` + `second` became `firstsecond`.
+  - **Thinking works in both directions.** `thinking.budget_tokens` becomes the reasoning budget
+    (upstream's default 10000 without one), so a thinking block can no longer use up the whole answer;
+    the answer carries the reasoning as a `thinking` block first, and streams it as `thinking_delta`s
+    closed with a `signature_delta`; `thinking` blocks of earlier turns go back to the model as
+    `reasoning_content`.
+  - **A request without `max_tokens` is capped at 4096**, as upstream, instead of running to the end
+    of the context, and `chat_template_kwargs` is passed on.
+  - **`tool_choice: {"type": "none"}` forbids tool calls**; it ran as `auto`. **`{"type": "tool",
+    "name": ...}` forces that tool**: it became `required` over every tool, as in upstream's
+    translation, so the model could call another one; it is passed on as OpenAI's named choice now. A
+    name the request does not carry, a `tool` choice without a name and any other type are `400`.
+  - **Tool calls without arguments**: `tool_use.input` is `{}` instead of `null`, and a `tool_use`
+    without `input` in the history is sent as `"{}"` -- `""` or `"null"` made llama.cpp refuse the whole
+    follow-up request.
+  - **Claude Code's prompt cache hits.** Its system prompt starts with a billing header whose `cch`
+    stamp changes on every request, so no prefix ever matched and every turn re-evaluated the whole
+    prompt; the stamp is normalized as upstream does.
+  - **A server error is an `api_error`**, a rejected request an `invalid_request_error`; every error
+    was called `invalid_request_error`, so a client deciding by the type whether to retry took a
+    server fault for its own.
+  - **The stop word is fetched alone**: the translated request asks for
+    `response_fields: ["stopping_word"]` with `verbose`, instead of upstream's whole debug object with
+    the rendered prompt.
+  - **A streamed answer opens one content block at a time.** When the model thought again after
+    text, the new `thinking` block was opened while the text block was still open, and the text that
+    followed was sent to the earlier block after the later one had stopped; the text block is closed
+    first now, so thinking and text alternate as separate blocks, as Claude itself streams them.
+  - **A `max_tokens` that is not a number is `400`** (the Messages API types it as an integer, and
+    upstream's schema refuses it); `"max_tokens": "100"` ran with the default 4096. A missing or
+    `null` value still gets 4096, and the refusal comes before a stream starts.
+- **OpenAI Responses (`/v1/responses`).**
+  - **Images and tool output parts reach the model**: `input_image` parts of a message and an
+    array-valued `function_call_output` (`input_text` / `input_image`) were flattened to their text or
+    to nothing.
+  - **The history is rebuilt the way upstream does**: text parts of one item are joined with a
+    newline; `reasoning` items become the `reasoning_content` of the assistant turn, where they were
+    turned into **user** messages; consecutive `function_call` items and a following assistant
+    `message` join the preceding assistant turn instead of opening one each; a `function_call`
+    without `arguments` is sent as `"{}"`; and a `refusal` part is kept as text instead of being
+    dropped.
+  - **Reasoning works in both directions**: `reasoning.effort` is passed on (`"none"` switches
+    thinking off), and the answer carries the reasoning as a `reasoning` item first, streamed as
+    `response.reasoning_text.delta`s.
+  - **The request fields the chat path reads are passed on** -- `top_k`, `seed`, `stop`, the penalties,
+    `chat_template_kwargs`, `response_format`, the cache fields, llama.cpp's sampling fields and
+    `logit_bias` / `n_probs`; only `temperature`, `top_p` and `max_output_tokens` got through.
+    **`text.format` (structured output) becomes `response_format`**, so a JSON schema constrains the
+    answer.
+  - **What cannot be served is refused with `400`** instead of answered as if it were absent:
+    `previous_response_id` (there is no response store; send the earlier turns in `input`),
+    `input_file` parts, an `input_image` without a URL (a `file_id` only), a tool output part that
+    is neither `input_text` nor `input_image`, a `function_call` whose `arguments` is not a string
+    (an object became `"{}"`), and an input item of an unknown type (`custom_tool_call`,
+    `item_reference`, ..., which became an empty user message), each with upstream's message but the
+    `function_call` one, which names the call where upstream says "Cannot determine type of 'item'".
+  - **An answer cut off by `max_output_tokens` is `incomplete`**, blocking and streamed: `status`
+    `incomplete` with `incomplete_details.reason` `max_output_tokens`, the message item `incomplete`
+    and `response.incomplete` as the last event, where it was reported `completed`. llama.cpp
+    reports a full context the same way, so the reason is the closest the API defines.
+  - **A streamed failure ends with `response.failed`** (with `error.code` / `error.message`) before
+    the `error` event; Responses clients such as Codex read the message only from there and reported
+    "stream closed before response.completed".
+  - **`created_at` is the current time**, not `0`, and a response that only calls tools has no empty
+    `message` item in front of the `function_call` items, as upstream and the streamed answer.
+- **Reranking.** `/rerank` accepts Hugging Face text-embeddings-inference requests (`texts` instead of
+  `documents`) and answers them in TEI's form, as upstream; `top_n: 0` returns no result instead of
+  all, a negative `top_n` is `400`, and the OpenAI-shaped answer carries `usage` (the native rerank
+  result passes `tokens_evaluated` on). A `top_n` written as a decimal (`2.0`, as a Python client
+  sends `len(docs) / 2`) is read like upstream's integer cast instead of keeping every result, so a
+  negative one is `400` in both spellings; `"documents": null` next to `texts` is served from
+  `texts`; and a native answer that is not a JSON array is a `500` with the reason instead of an
+  empty result list. `LlamaModel.rerank(String, String...)` returns its map in the order of the
+  documents instead of hash order.
+- **Java API.**
+  - **`InferenceParameters.withMessages(List)` keeps `tool_calls` and `tool_call_id`**,
+    which only `ChatRequest` added, so a tool round built through it sent an assistant turn without
+    its calls and a result without its id. Empty tool-call arguments are written as `"{}"`, which
+    llama.cpp parses (`""` failed the whole follow-up request with `400`).
+  - **Five "`null` clears" withers clear.** `withPrompt`, `withInputPrefix`, `withInputSuffix`,
+    `withGrammar` and `withToolChoice` returned the request unchanged for `null`, so
+    `withGrammar(g).withGrammar(null)` still sent the grammar.
+  - **Infilling through `complete()`, `completeWithStats()` and `generate()` works as documented.**
+    `withInputPrefix` / `withInputSuffix` -- the way the README describes infilling -- only switched
+    the task type: prefix and suffix were dropped without a word and the (mostly empty) prompt was
+    completed, and the blocking `handleCompletions` did not look at the two fields at all. The raw
+    completion paths now build the prompt around the model's fill-in-the-middle tokens exactly as
+    `handleInfill` does (an optional prompt is the text after the middle); a request with only one of
+    the two fields is an `InvalidRequestException`, a model without FIM tokens a `LlamaException`, as
+    on `handleInfill`. The same for the C API (`jllama_completions` and the raw stream) and
+    `llama-ffm`.
+  - **`withMessages(...)` takes effect on `generate()`, `complete()` and `completeWithStats()`.** Only
+    the chat paths read `messages`, so the raw paths completed the prompt next to them -- mostly the
+    empty one -- or failed with "key 'prompt' not found" without one. The messages are now rendered
+    through the model's chat template, as `applyTemplate` renders them, and that prompt is completed;
+    the other fields stay completion fields, and the media of array-form content reach the model. A
+    non-empty prompt or the infill fields next to the messages are an `InvalidRequestException`
+    instead of one of them being dropped. So is a chat field next to them that only the chat parser
+    reads and a raw completion cannot apply -- `response_format`, `tools`, `tool_choice`,
+    `parallel_tool_calls`, `top_logprobs`, `thinking_budget_tokens` (`withResponseFormat`,
+    `withToolsJson`, `withToolChoice`, `withParallelToolCalls`) -- with a message that names the field
+    and points to the chat path; `json_schema`, `grammar` and `chat_template_kwargs` stay allowed. The
+    completion also stops at the stop strings a chat template adds of its own (``,
+    `<|role_end|>`), next to the request's `stop`, as a chat completion does; it ran past them. And a
+    reasoning budget next to the messages (`withReasoningBudgetTokens`, `reasoning_budget_tokens`)
+    takes effect through the template's thinking tags, as on the chat paths: once it is spent, the
+    model is made to close its thinking block with the template's end tag (``); without the
+    tags, which the raw paths dropped, the budget did nothing. A template without thinking tags leaves
+    the request unchanged, as a chat completion does. The tokens a template preserves come along too:
+    an end tag the vocabulary holds as a special token (Qwen3's ``) reads as text in the
+    content, where it was an empty piece; a request's own `preserved_tokens` stay in force beside
+    them. Without messages the tags come from the request (see Added); a budget given tags but no
+    `reasoning_budget_message` now forces the end tag behind the server's default message, as the
+    chat path does -- before, it counted and forced nothing. The same for the C API and `llama-ffm`.
+  - **JSON values are taken as given or refused, never cut short.** A `ToolDefinition` schema with
+    trailing content (`{"type":"object"} {"required":["x"]}`) was sent as its first value, and a
+    `chat_template_kwargs` value holding two JSON values as the first of them. A schema that is not
+    exactly one JSON value -- trailing content, an empty or an unparsable string -- is refused now
+    with an `IllegalArgumentException` naming the tool (a tool without parameters declares
+    `{"type":"object","properties":{}}`); a `chat_template_kwargs` value that is not one JSON value,
+    an empty one included, is passed on as a string, where an empty value became `null`.
+  - **`LlamaIterator` stays finished after `cancel()` from another thread**; a result that arrived
+    afterwards revived it, and the next `next()` failed with "Task not found".
+  - **`TextToSpeech.close()` waits for a running `synthesize()`** instead of freeing the engine under
+    it (a use-after-free, i.e. a JVM crash); syntheses still run side by side.
+  - **`NativeServer` start, `isRunning()` and `close()` are serialized**: two `close()` calls at once
+    (a shutdown hook and try-with-resources) could stop and free the native server twice, and
+    `isRunning()` could read a freed handle.
+  - **A failed `RpcServer.start` stops the server.** A stop that arrived before the native server bound
+    its socket was lost, so after a start timeout or an interrupt the server kept listening on a daemon
+    thread although `start` had thrown; the stop is now repeated until the thread ends. If the thread
+    still lives 30 s after the stop, `start` says so (instead of "could not listen ... is the port
+    already in use?") and the single-instance slot stays taken until the thread ends -- it used to be
+    freed, so a second server could start on the same process-wide state. `close()` holds it the same
+    way; it throws a `LlamaException` when the native server has not ended 30 s after the stop, and logs
+    a warning when only the thread is still alive then. The C API and `llama-ffm` keep such a stop as
+    well: `jllama_rpc_server_stop` / `FfmRpcServer.stop` during a running serve hold the request until
+    the serve returns (failing after 30 s), and a stop with no serve running stays a no-op.
+  - **`RouterClient`**: `awaitModelLoaded` with `Long.MAX_VALUE` overflowed into an immediate timeout,
+    and with `0` reported a timeout without asking; it polls at least once now, counts a `sleeping`
+    model as loaded (as the router itself does), and finds a model by its alias; an IPv6 literal host
+    (`"::1"`) is bracketed in the URL instead of failing with `MalformedURLException`, and in
+    `toString()` too.
+  - **`LlamaTrainer.finetune` accepts any Unicode**: a character outside the Basic Multilingual Plane
+    (an emoji) in the training text or a path failed the whole run with "invalid training config".
+  - **`GgufInspector.read` rejects nested arrays with an `IOException`**, as llama.cpp's reader does; a
+    crafted file ended it with a `StackOverflowError`.
+  - **`Session.fork` refuses a negative slot id before touching the model**, with the constructor's
+    `IllegalArgumentException`, instead of writing the slot file and failing in `restoreSlot`.
+  - **An offline load (`--offline`) of a path the file system cannot represent** throws the documented
+    `ModelUnavailableException` instead of an `InvalidPathException`; a path the JVM cannot encode in
+    its file-name charset -- a non-ASCII model name in a JVM running with the POSIX locale
+    (`LC_ALL=C`) -- is no longer reported as missing although the file exists, but left to the native
+    loader, which reads paths as UTF-8 and loads it.
+  - **A malformed stream envelope fails the stream instead of ending it.** `ChatStreamChunkParser.feed`
+    took an unparsable, empty or blank envelope for the end of the stream and one without `stop` for
+    "continue" (a polling loop that would never end), so a transport fault looked like a short answer,
+    also through `llama-langchain4j`. It reads the envelope strictly now and throws
+    `ChatStreamChunkParser.MalformedEnvelopeException` naming the reason and an excerpt; `LlamaModel`'s
+    streams turn it into a `LlamaException`, which `OpenAiCompatServer` answers with `500`.
+  - **A vocab-only model refuses what it cannot answer instead of ending the JVM.** A model loaded with
+    `ModelParameters.setVocabOnly()` has the tokenizer and no inference server, but only the model
+    meta, `encode`/`decode` and `/v1/systemone` knew it: `applyTemplate`, `complete`, `chatComplete`
+    and the other completion paths crashed the JVM (`SIGSEGV` in `llama_vocab::token_bos()`),
+    `getMetrics`, the slot actions and the LoRA calls waited forever. Every other operation now throws
+    a `LlamaException` "`` is not available in vocab-only mode", decided once in the C API,
+    so `llama-ffm` and C callers get the same `JLLAMA_STATUS_ERROR`.
+- **`LlamaLoader` deletes only its own files from the temp directory.** The clean-up of the pre-6.0.0
+  layout deleted, recursively, every entry of `java.io.tmpdir` (or `net.ladenthin.llama.tmpdir`) whose
+  name starts with `jllama`, `llama` or `ggml` -- a `/tmp/llama.cpp` checkout, a `/tmp/ggml-models/`
+  folder, a downloaded `/tmp/llama-2-7b.Q4_K_M.gguf`. It removes the regular files the old layout
+  wrote now -- the library names with a library suffix (`.so`, `.dll`, `.dylib`, `.jnilib`, `.metal`)
+  and, once they are ten minutes old, the `..tmp` copies an interrupted extraction of
+  that layout left -- and nothing else.
+- **`llama-ffm`.** `FfmTextToSpeech.close()` waits for a running `synthesize()` and
+  `FfmNativeServer.close()` for a running `isRunning()`, as their JNI twins do, instead of freeing the
+  native object under them; and an empty, non-null string array returned by the library is freed.
+- **`llama-langchain4j`.** The blocking `JllamaChatModel` carries the reasoning as
+  `AiMessage.thinking()` like the streaming model; the streamed response carries the token usage (the
+  request now asks for the usage chunk); a streamed tool call no longer fails at its first fragment
+  without arguments or without a name (langchain4j's `PartialToolCall` refuses both, the fragment
+  still joins the assembled call); and several `TextContent`s of a user message are joined with a
+  newline instead of being glued together. Arguments that arrive before a call's name are reported
+  through `onPartialToolCall` once the name is known, so the partial calls of a tool call add up to
+  its complete arguments (`ty":1}` arrived where `{"city":1}` was sent).
+- **A scaled LoRA adapter or control vector loads at all.** `ModelParameters.addLoraScaledAdapter` and
+  `addControlVectorScaled` rendered their value as `FNAME,SCALE`, while llama.cpp has read these options
+  as `FNAME:SCALE,...` since b7448 (#18056): the comma is the list separator, the colon the field
+  separator. Every value they emitted was rejected by the argv parser (`invalid stof argument`), so the
+  model did not load and the caller saw only `Failed to parse model parameters`. They now render
+  `FNAME:SCALE`. One upstream limit comes with it and is refused loudly rather than silently: the parser
+  splits each item on *every* colon and demands exactly two parts, so an absolute Windows path cannot be
+  expressed — `addLoraScaledAdapter` throws `IllegalArgumentException` naming the cause and the two ways
+  out (a relative path, or `addLoraAdapter` plus `LlamaModel.setLoraAdapter` after the load).
+- **The repeatable adapter options accumulate instead of replacing.** `addLoraAdapter`,
+  `addLoraScaledAdapter`, `addControlVector` and `addControlVectorScaled` all say "can be repeated" and
+  all four silently dropped everything but the last call, because the parameter set is keyed by the wire
+  string. They now append to one comma-separated value, as llama.cpp reads these options, and a value
+  carrying a comma or a quote is written as a quoted CSV field.
+- **`/v1/models` and the answers name the model the same without `--model-id` too.** The id was passed on
+  as llama.cpp's `--alias` only when the flag was given; without it `/v1/models` reported the model's file
+  name while every answer and stream chunk reported the full model path, because upstream's fallback for
+  the model name resolves to the path for a local file. `OpenAiCompatServer` now always sets the alias
+  from the advertised id, so the two agree and no local path reaches an answer. An embedding caller that
+  loads its own `LlamaModel` still has to set `ModelParameters.setAlias(…)` itself — the server cannot,
+  the model is already loaded; `OpenAiServerConfig.getModelId()` says so. The Atmosphere agent's
+  in-process server had the same gap: its answers named the absolute model path while `/v1/models`
+  listed `--model-id` (default `local-model`); it sets the alias now, and with `--model` refuses a
+  `--model-id` with a comma, an empty one and one with leading or trailing whitespace, as
+  `OpenAiServerCli` does.
+- **A streamed `/v1/completions` is upstream's stream.** `OpenAiCompatServer` built the chunks of a
+  streamed completion itself from the Java generator: no `usage` on the last chunk, no probabilities
+  (a request for them was refused with a pointer to `stream: false`), and a prompt given as a token
+  array refused
+  while the same request without `stream` was accepted. The body now goes to llama.cpp verbatim, as
+  the blocking request does, and the chunks are upstream's own `text_completion` events. New
+  `LlamaModel.streamCompletionsOai(String, Consumer)`, the C function
+  `jllama_completions_oai_stream_request` (received with `jllama_chat_completions_stream_receive`)
+  and `FfmLlamaModel.completionsOaiStream`.
+- **OpenAI answers carry an id and the model name.** Every native OpenAI answer and stream chunk had
+  `"id": ""` and `"model": ""`; upstream gives each completion a `chatcmpl-` id (one per stream) and
+  names the model it runs (the alias, else the model's name). They do now. New
+  `ModelParameters.setAlias(String)` (`--alias`), and `OpenAiCompatServer --model-id` passes the id on
+  as the alias, so the answers name the model `/v1/models` lists.
+- **Several prompts in one request are refused instead of answered with the first.** A task carries
+  one prompt, and `/v1/completions` (and `handleCompletions`, the raw stream) took the first of
+  `"prompt": ["a", "b"]` and dropped the rest without a word -- two prompts, one choice. Upstream
+  answers each; here the request is now an invalid request (`400`) naming the count. A token array is
+  one prompt and stays accepted. **The same for `"n"` above 1:** llama.cpp's schema accepts up to the
+  slot count and answers the further completions in child tasks its server creates, which this
+  library's engine never did, so `n: 2` came back as one choice on every native path -- the C API,
+  `llama-ffm`, `LlamaModel.handle*`, both `/v1/completions` routes and the chat paths; the server's
+  request mapper had dropped `n` on the chat routes as well. It is an invalid request now, checked in
+  one place for the blocking, streamed and chat paths.
+- **`/v1/messages` reports the matched stop word as `stop_sequence`**, blocking and streamed, and
+  `stop_reason` is `"stop_sequence"` then, as the Messages API specifies (upstream reports `end_turn`
+  there); the word was always `null`, and an answer stopped by it said `end_turn`. The OpenAI answer the
+  route translates from does not carry the word, so the translated request asks for llama.cpp's `verbose`
+  answer and reads `__verbose.stopping_word`, which never reaches the client. New
+  `InferenceParameters.withVerbose(boolean)` (`RequestField.VERBOSE`), and the OpenAI mapper forwards a
+  client's `verbose`.
+- **A streamed request that fails before its first chunk is answered with the error status.**
+  `OpenAiCompatServer` sent `200` and the stream's headers before the engine ran, so a request
+  llama.cpp refused at once arrived as `200` with an error event -- on all five streaming routes
+  (`/v1/chat/completions`, `/v1/completions`, `/api/chat`, `/v1/messages`, `/v1/responses`). Upstream's
+  server reads the first result before it starts a stream; this one now does the same: the response
+  starts with the first write (a chunk, the protocol's opening events or a heartbeat), and a failure
+  before it is `400` or `500` with the route's own error body, as without `stream` -- an `IOException`
+  an `OpenAiBackend` reports before the first chunk included, which was taken for a client that had
+  gone away and ended the request with a closed connection and no status line. One visible effect:
+  the Atmosphere agent now reports such a failure instead of ending the turn with an empty answer.
+  **After the stream started**, an `IOException` of the backend still counted as the client having
+  gone away: no error event, the stream simply ended and only a DEBUG line was logged. Only the
+  server's own failed write counts as a disconnect now; every other failure is logged at WARN and
+  ends the stream with the route's error event (a `data:` error object, an Ollama `{"error": ...}`
+  line, Anthropic `event: error`, Responses `response.failed` and `event: error`).
+- **Error messages with a 4-byte character reach Java intact.** The JNI shells handed messages to
+  `ThrowNew` and `NewStringUTF`, which read Modified UTF-8; a message quoting a path or request text
+  with a supplementary-plane character (an emoji) was mangled on HotSpot -- a trainer message for
+  `missing-😀.gguf` came back as `missing-`, four wrong characters and `.g` -- and aborts the process under
+  Android's CheckJNI. A non-ASCII message now crosses as bytes through `String(byte[], "UTF-8")`
+  (`jni_utf8.hpp`), on every path that carries llama.cpp's text: the C API status, the model load,
+  the JNI exception boundary, `NativeServer`, `RpcServer` and `LlamaTrainer`. Called while a Java
+  exception is already pending (after a failed `GetStringUTFChars`, say), both helpers leave that
+  exception in place, as the JNI exception boundary already did, instead of replacing it with a less
+  precise one or calling JNI functions the specification forbids in that state, which Android's
+  CheckJNI aborts on.
+- **A LoRA adapter change no longer answers from the old cache** (`patches/0021`). After
+  `setLoraAdapter(s)` -- and after a request with its own adapters -- a slot reused the prompt cache it
+  had computed under the previous scales and answered like neither adapter set (measured with
+  llama.cpp's own LoRA test pair: a third, unrelated text until the slot was erased). Upstream clears
+  the cache when a request names its own adapters but not when the server's adapters change; the patch
+  applies the same rule there, which also fixes upstream's `POST /lora-adapters` on the embedded
+  server. Every slot starts with the server's adapters, so a slot's first request on a server
+  started with `--lora` keeps a prompt restored into it (`restoreSlot`) or loaded from the prompt
+  cache instead of treating it as computed without adapters and throwing it away.
+  `setLoraAdapters`' Javadoc no longer tells callers to erase the slots; it names the one cache
+  that still does not record adapters, the host-memory prompt cache (`setCacheRamMib(0)` disables it).
+- **`ServerMetrics.getCumulativeUsage()` counts the cached prompt tokens.** It returned
+  `Usage(processed, predicted)`, so `getPromptTokens()` was the evaluated tokens only and
+  `getCachedTokens()` always `0`, against `Usage`'s own contract (prompt tokens include the cached
+  ones, which are their subset). The method's Javadoc gave the reason "upstream exposes no cumulative
+  cached total", which stopped being true when `getMetrics()` started carrying
+  `n_prompt_tokens_cached_total`. Now `getPromptTokens()` is evaluated + cached,
+  `getCachedTokens()` the cached ones and `getProcessedPromptTokens()` the evaluated ones -- read the
+  same way as the `usage` of a response. A caller who read `getPromptTokens()` as "evaluated" reads
+  `getProcessedPromptTokens()` or `getCumulativeProcessedPromptTokens()` now. `getWindowUsage()` is
+  unchanged: upstream keeps no cache count for the window.
 - **The GitHub `snapshot` pre-release and the versioned releases carry what Maven Central carries,
   and the `snapshot` release no longer accumulates.** The collect steps copied `*.jar` / `*.jar.asc`
   and the bare AARs, so every `.pom` with its `.pom.asc` was missing, `llama-platform` (a pom-only
@@ -738,6 +1448,14 @@ the first two items break every existing consumer at upgrade.
   passes `-Dmaven.repository.credentialScope=id`, Maven's own switch back to id-only matching, until a
   setup-java release can write `` (`mvn-server-repository-origins`, merged upstream
   but not yet released).
+- **`NativeLibraryLoadSmokeTest` reported the real library as llama.cpp `bTEST` (or as `{}`) when the
+  Java suite ran in one Surefire fork**, depending on the class order. `LlamaModel.getJllamaBuildInfo()`
+  returns the build file of the last successful load, and two loader test classes load fixtures:
+  `BackendLoadTest` really loads its dummy library, `LlamaLoaderTest` reports loads from directories
+  with a fixture build file or none. Run after a real load, they left the fixture's record standing for
+  the real library. Both classes now put back the record they found (`LlamaLoader.restoreBuildInfo`,
+  package-private, for the loader tests only). The shipped library behaves as before: only a
+  successful `System.load` sets the record, a refused or failed `initialize()` leaves it as it was.
 - **`SessionConcurrencyTest` timed out on `Java Tests macOS 15 arm64 (Metal)` from llama.cpp b11457 on**
   (green through b11320, red in both runs since). Only this class slowed down -- about 130x, from
   0.45 s to 57-110 s per token, while every other class ran as fast as before. It is the one test
diff --git a/CLAUDE.md b/CLAUDE.md
index 7e3429d5f..b97c95b78 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
 
 Java bindings for [llama.cpp](https://github.com/ggerganov/llama.cpp) via JNI, providing a high-level API for LLM inference in Java. The Java layer communicates with a native C++ library through JNI.
 
-Current llama.cpp pinned version: **b11639**
+Current llama.cpp pinned version: **b11815**
 
 ## Natives jars: library jars and GPU module jars (`.github/natives.csv`)
 
@@ -213,7 +213,7 @@ measurements behind the Windows step are in `docs/handover/local-agent-report-b1
   a `llama-common`/`cpp-httplib` that came out shared (one more file to ship). On the static
   platforms (macOS, s390x) all of it stays inside `libjllama`. **Every ggml default llama.cpp's own
   CMakeLists changes before its `add_subdirectory(ggml)` is repeated** -- `GGML_LLAMAFILE_DEFAULT`
-  and `GGML_CUDA_GRAPHS_DEFAULT` at b11639 -- because adding ggml first skips that block; re-check with
+  and `GGML_CUDA_GRAPHS_DEFAULT` at b11815 (unchanged since b11639) -- because adding ggml first skips that block; re-check with
   `grep -n _DEFAULT CMakeLists.txt` at the new tag on every bump. The second one was missing once:
   ggml's own default for CUDA graphs is OFF, and a CUDA module built without it lost two thirds of
   its token generation (265.6 -> 88.8 tg128 on an RTX 3070, prompt processing -20%; Vulkan, which
@@ -951,8 +951,8 @@ vendor runtime.
 |---|---|---|---|
 | `rocm-linux-x86-64` | `GGML_HIP=ON -DCMAKE_HIP_COMPILER=… -DGPU_TARGETS=…` | `ubuntu-latest` + ROCm 10 TheRock wheels (pip, `rocm-sdk path`) | `Linux/x86_64/rocm/libggml-hip.so` |
 | `rocm-windows-x86-64` | `GGML_HIP=ON` | `windows-2022` + ROCm 10 TheRock wheels (pip) | `Windows/x86_64/rocm/ggml-hip.dll` |
-| `sycl-linux-x86-64` | `GGML_SYCL=ON -DGGML_SYCL_F16=ON` (`icx`/`icpx`) | `ubuntu-latest` + Intel oneAPI apt | `Linux/x86_64/sycl/libggml-sycl.so` |
-| `sycl-windows-x86-64` | `GGML_SYCL=ON -DGGML_SYCL_F16=ON` (`icx`) | `windows-2025-vs2026` + oneAPI installer | `Windows/x86_64/sycl/ggml-sycl.dll` |
+| `sycl-linux-x86-64` | `GGML_SYCL=ON -DGGML_SYCL_F16=ON` (`icx`/`icpx`) | `ubuntu-latest` + Intel Deep Learning Essentials 2026.1.4 + oneDNN 2026.0.2 (offline installers) + Level Zero 1.33.1 debs, as upstream's `ubuntu-24-sycl` | `Linux/x86_64/sycl/libggml-sycl.so` |
+| `sycl-windows-x86-64` | `GGML_SYCL=ON -DGGML_SYCL_F16=ON` (`icx`) | `windows-2025-vs2026` + Deep Learning Essentials 2026.1.4 + oneDNN 2026.0.2 (offline installers) + the Level Zero SDK zip, as upstream's `windows-sycl` | `Windows/x86_64/sycl/ggml-sycl.dll` |
 | `opencl-windows-aarch64` | `GGML_OPENCL=ON …ADRENO_KERNELS=ON` (clang-cl, `GGML_OPENMP=OFF`) | `windows-11-arm` (arm64 CPU job's toolchain) | `Windows/aarch64/opencl/ggml-opencl.dll` |
 | `openvino-linux-x86-64` | `GGML_OPENVINO=ON` | `ubuntu-latest` + OpenVINO apt | `Linux/x86_64/openvino/libggml-openvino.so` |
 | `openvino-windows-x86-64` | `GGML_OPENVINO=ON` | `windows-2025-vs2026` + OpenVINO archive | `Windows/x86_64/openvino/ggml-openvino.dll` |
@@ -992,8 +992,18 @@ jar barely shrinks (zip already compressed the code); what shrinks is the extrac
 **SYCL is fp16 on both OSes, one jar each**, as upstream's SYCL release builds (`GGML_SYCL_F16=ON`).
 The second Linux precision (`sycl-fp32-linux-x86-64`, fp32 accumulation) was dropped with the modular
 natives: nobody had asked for it, and two modules of the same backend on one classpath would both
-register the same device. **Both SYCL jobs install oneDNN** (Linux `intel-oneapi-dnnl-devel`, Windows
-`intel.oneapi.win.dnnl`): since llama.cpp #29107 (b11636) `GGML_SYCL_DNN` (default ON) fails the configure
+register the same device. **Both SYCL jobs install upstream's toolchain, pinned by installer URL**
+(`KEEP IN SYNC WITH UPSTREAM` in `publish.yml`, read off `release.yml`'s `ubuntu-24-sycl` / `windows-sycl` jobs
+at the pinned tag; the bump runbook lists it): Intel's Deep Learning Essentials bundle (DPC++, MKL, TBB) plus
+the separate oneDNN installer (b11654, #30131 -- until the b11800 bump the Linux job took the same components
+from Intel's apt repository, unpinned, and the Windows job the 2025.3.3 bundle that still carried oneDNN), and
+the Level Zero loader/SDK upstream's jobs have had since #21597 / #29273 (`GGML_SYCL_SUPPORT_LEVEL_ZERO_API`,
+default ON, links `ze_loader` when found and only warns without it). **The Level Zero half changed the shipped
+modules**: the ones built from the apt packages / the 2025.3 bundle never found a loader (run 38870889665's
+package job listed none), so both modules import it now -- `libze_loader.so.1` / `ze_loader.dll`, the Intel GPU
+driver's -- and the Windows module needs the oneAPI 2026 runtime (`sycl9.dll`, `mkl_sycl_blas.6.dll`) instead of
+2025.3's `sycl8.dll` / `mkl_sycl_blas.5.dll`; the README classifier table names both. **oneDNN is required**: since
+llama.cpp #29107 (b11636) `GGML_SYCL_DNN` (default ON) fails the configure
 when oneDNN is missing or is a CPU-only build, where it used to warn and build a module whose flash attention
 runs the slower MKL path -- which is what the Linux module had been until the b11639 bump. The module then
 needs `libdnnl.so.3` / `dnnl.dll` at run time, from the same oneAPI environment (`setvars.sh`) as MKL.
@@ -1173,7 +1183,7 @@ needs no extra step here, `build-webui` re-reads the tag and rebuilds the matchi
 ships no UI):
 ```bash
 # needs node/npm + network for the asset build; the embed step is plain cmake -P
-git clone --depth 1 --branch b11639 https://github.com/ggml-org/llama.cpp /tmp/lc
+git clone --depth 1 --branch b11815 https://github.com/ggml-org/llama.cpp /tmp/lc
 ( cd /tmp/lc/tools/ui && npm ci && npm run build )
 mkdir -p webui-generated /tmp/ui-gen
 cmake -DUI_SOURCE_DIR=/tmp/lc/tools/ui -DUI_BINARY_DIR=/tmp/ui-gen \
@@ -1213,7 +1223,7 @@ cache lives in **Depot Cache** over sccache's **WebDAV** backend:
 - `SCCACHE_WEBDAV_TOKEN: ${{ secrets.DEPOT_TOKEN }}` — a Depot **organization** token, stored
   as the repo secret **`DEPOT_TOKEN`**.
 
-Because `sccache` is **content-addressed** and llama.cpp is pinned (`JLLAMA_GIT_TAG b11639`), the
+Because `sccache` is **content-addressed** and llama.cpp is pinned (`JLLAMA_GIT_TAG b11815`), the
 ~280 upstream object files are byte-identical every run, so a warm cache recompiles only the
 *changed* files. Depot's cache is **shared across all branches** (unlike GitHub's
 per-branch `actions/cache`), so every branch builds incrementally; a `b` version bump
@@ -1443,32 +1453,34 @@ Current patches:
 | `0001-win32-arg-parse-embed-guard.patch` | Windows JNI regression from llama.cpp **#24779** (introduced b9739): on Windows `common_params_parse` re-derived argv from the **process** command line (`GetCommandLineW`) and adopted it, so an embedded/JNI caller (`java.exe`) lost its `--model …` args → "Failed to parse model parameters". b9789 narrowed the unconditional override to a **count-guard** (`if (static_cast(utf8.buf.size()) == argc) { argv = utf8.ptrs.data(); }`), but that is exactly the variant the project already found breaks its Windows server-integration tests (when the embedded argv length coincides with `java.exe`'s). The patch carries the **complete upstream change** (so it can be submitted to llama.cpp verbatim and then dropped here): **(1)** `common_params_parse` parses **exactly the argv it is given** (no `GetCommandLineW` magic) and a new `common_params_parse_main()` wrapper holds the UTF-8 recovery for the standalone tools' `main()` (`common/arg.{cpp,h}`); **(2)** the **~34 standalone `main()` call sites** (every `common_params_parse(argc, argv, …)` across `tools/*`, `examples/*` and the `tests/*` programs) flip to `common_params_parse_main()`; **(3)** a `tests/test-arg-parser.cpp` regression case pins that `common_params_parse` honors a caller-supplied argv. The embedded caller (`jllama.cpp`) keeps calling `common_params_parse` and is never overridden. **Our subproject build compiles only the `arg.{cpp,h}` core** — `LLAMA_BUILD_TOOLS`/`LLAMA_BUILD_TESTS` are OFF for a FetchContent subproject — so the flips + test are applied-but-not-compiled here; they were validated via a one-off `-DLLAMA_BUILD_TOOLS=ON -DLLAMA_BUILD_TESTS=ON` build (the new test compiles and its asserts pass; `test-arg-parser`'s only red there is the live `ggml.ai` download check, which is sandbox-network, not the patch). Because it spans **35 files** it must be refreshed on every llama.cpp bump (the applier fails loud). **Refreshed at the b10679 bump:** upstream rewrote `tests/test-save-load-state.cpp`'s `main()` to take a `--models DIR` option, which it strips itself into a `filtered_argv` before calling `common_params_parse(fargc, filtered_argv.data(), …)`. That call site therefore stopped qualifying for the `_main()` flip — by this patch's own rule a caller that builds its own argv must use `common_params_parse` directly, so its argv is kept — and the hunk was **dropped** rather than refreshed (37 → 36 files). Caveat for whoever submits this upstream: that `main()` now filters a possibly-mojibake Windows argv *before* any UTF-8 recovery, so the fully correct upstream form there is recover-then-filter, not a one-line flip. It is out of scope for the downstream carry because `LLAMA_BUILD_TESTS` is OFF here, so the file is never compiled. **Refreshed the same way at the b11236 bump:** upstream #29426 rewrote `tests/test-recurrent-state-rollback.cpp`'s `main()` into the identical `--models DIR` / `filtered_argv` shape, so that hunk was dropped too (36 → 35 files); the same recover-then-filter caveat applies to it. **Still required at b10679, verified rather than assumed:** `common_params_parse` in pristine `b10679:common/arg.cpp` still carries the `#ifdef _WIN32` count-guarded `argv = utf8.ptrs.data()` override, and `common_params_parse_main` appears nowhere in `b10679:common/arg.h` — upstream has not adopted the fix. The upstream-facing write-up, including a standalone reproducer that makes llama.cpp's own `test-arg-parser` fail on unmodified `master`, lives in [docs/upstream-investigation-win32-argv-substitution.md](docs/upstream-investigation-win32-argv-substitution.md). **Reported upstream as [ggml-org/llama.cpp#26416](https://github.com/ggml-org/llama.cpp/issues/26416)** (2026-08-01, label `bug-unconfirmed`, first bad commit `508a475`); the issue asks which of the two directions the maintainers prefer before a PR is opened, so this patch stays downstream until they answer. **Refreshed at the b11612 bump:** #30023 (`a17e94d22`, "tests : consolidate context state tests into test-llama-context") deleted `tests/test-state-restore-fragmented.cpp`, so its hunk was dropped (36 → 35 `diff --git` headers); the consolidated `tests/test-llama-context.cpp` has the `--models DIR` / `filtered_argv` `main()` shape, so by this patch's own rule it keeps calling `common_params_parse` directly and gets no flip. Still required, verified at pristine b11612: `common/arg.cpp:1288` carries the count-guarded `argv = utf8.ptrs.data()` override and `common/arg.h` has no `common_params_parse_main`. |
 | `0002-server-preserve-caller-load-progress-callback.patch` | Load-progress-callback regression introduced in llama.cpp **b9789**: `server_context::load_model` (`tools/server/server-context.cpp`) now **unconditionally** installs the server's own load-progress reporter on `params_base.load_progress_callback` immediately before `common_init_from_params`, clobbering any callback the embedding caller already set. libjllama's `LoadProgressCallback` feature wires `common_params.load_progress_callback` to a JNI trampoline *before* calling `load_model`, so the bump silently killed it — `LoadProgressCallbackTest` saw zero progress updates and the abort-on-`false` path never threw. The patch guards the assignment with `if (params_base.load_progress_callback == nullptr)`, so the server installs its own reporter **only when the caller hasn't** — a caller-supplied callback survives and fires during load. Standalone `llama-server` (no caller callback, so the field is null) is unaffected. Same JNI-vs-standalone divergence class as `0001`. **The guard is `== nullptr || == load_progress_callback`, and the second disjunct must never be dropped:** `load_progress_text` is a **local** of `load_model()`, and upstream re-assigns both fields on every call so the `user_data` always points at the current frame. `load_model()` runs a **second** time when resuming from the sleeping state (`--sleep-idle-seconds`), and by then `params_base` holds *our own* callback from the first load — a bare nullptr check skips the re-assignment and leaves `user_data` pointing into a **dead stack frame**, which segfaults inside `load_progress_callback()` on the first request after an idle window. That was a latent defect in this patch from the day it was written; only a second `load_model()` can reach it, and nothing exercised sleep until `IdleSleepWakeIntegrationTest` was added. |
 | `0003-pr22393-server-add-slot-prompt-similarity-getter-setter.patch` | **Upstream-PR carry** of [ggml-org/llama.cpp#22393](https://github.com/ggml-org/llama.cpp/pull/22393) ("server : add slot_prompt_similarity getter/setter"). Purely additive: adds `server_context::get_slot_prompt_similarity()` / `set_slot_prompt_similarity(float)` (`tools/server/server-context.{cpp,h}`) so an embedding/JNI caller can query and tune the slot-selection threshold at runtime without reloading the model. Verbatim copy of the PR, which **upstream closed without merging** (rejected as exposing unsafe internal state — see the patch header). Carried permanently; it will not be droppable via a version bump. |
-| `0007-server-attach-http-frontend.patch` | **Adds `llama_server_attach(argc, argv, server_context&)`** so the `NativeServer` *attach mode* can serve an **already-loaded `LlamaModel`** over the upstream HTTP frontend — no second model load, no `start_loop()`; the LlamaModel's worker keeps driving the shared `server_context` and the HTTP routes post tasks to its queue (the queue is the synchronization point). Mechanically: (1) extracts the **pure core route table** (`health` … `slots`) out of `llama_server()` into `static void llama_server_register_common_routes(ctx_http, routes)` (shared, so the two entry points cannot drift on the core endpoint set). **Scope note (narrowed at the b10154 bump):** the helper deliberately carries **only** the stable, state-independent route table — **not** the resumable-streaming routes (their handlers differ between router / non-router), the GCP-compat shim, or the experimental **CORS-proxy / MCP-server / built-in-tools** wiring. b10154 (upstream MCP-server support) moved the streaming routes into the middle of that block and coupled tools/CORS to a per-call `server_mcp mcp_mgr` lifecycle, so the earlier contiguous "route-table + CORS-proxy + tools" extraction is no longer possible; `llama_server()` keeps all of that inline, **byte-identical to upstream b10154** (only the route-table block is factored out). (2) adds `llama_server_attach`, which parses only the HTTP-side argv via `common_params_parse`, starts the stream-session GC + `server_http_context`, registers the common route table, the **non-router** resumable-streaming handlers (upstream b10154 paths `/v1/stream` GET/DEL + `/v1/streams/lookup` POST), the GCP-compat shim, and **403 "disabled" stubs for `/cors-proxy` + `/tools`** (attach mode does not wire the experimental CORS-proxy / MCP / built-in-tools host — those belong to a full `llama-server`, not an embedded model), marks ready immediately (model already loaded), and blocks on the HTTP thread until `llama_server_request_shutdown()` — never calling `common_init()`, backend init, `ctx_server.terminate()` or `llama_backend_free()` (the embedding caller owns those). Applies after `0001`+`0006` (same file); closes the "NativeServer — reuse an already-loaded LlamaModel" TODO. Upstream-submittable ("server: let embedding callers attach the HTTP frontend to an existing server_context"). **Refreshed at the b10519 bump:** upstream #26347 dropped the API key from the `/models` + `/v1/models` public-endpoint set and deleted the two trailing `// public endpoint (no API key check)` comments on those route registrations. Those two lines sit inside this patch's route-table removal block, so `git apply` failed ("patch does not apply", `server.cpp:258`) at **every** tag from b10519 on; the fix was to drop the now-wrong comment from all four affected lines (2 on the `-` side, 2 in the extracted helper on the `+` side), keeping the helper byte-identical to the block it replaces. **This is the invariant to re-check on every bump:** the `+` side of `llama_server_register_common_routes()` must stay a verbatim copy of the route table it factors out of `llama_server()`. **Refreshed at the b11104 bump** (upstream #28690, multi-address `--host`): `server_http_context` lost its single `thread` and `listening_address` members in favour of `join()` and a `listening_addresses` vector, one listener thread per bound address. The patch still *applied* cleanly there — only its own `+` lines named the removed members — so the applier could not see it; `llama_server_attach` now logs every address and blocks in `ctx_http.join()`, exactly as upstream's `llama_server()` does. **Refreshed at the b11361 bump** (#29818 adds `POST /v1/systemone` to the route table: one line, added on both sides so the helper stays verbatim) **and at b11401** (#29895 adds a `server_child &` overload of `llama_server()` beside the declarations and a `server_child child;` at the top of the argv entry point, the context of `0007`'s first two hunks; only context moved, the `+`/`-` lines are unchanged). **Changed in 5.2.x, not a refresh: `llama_server_attach(argc, argv, server_context &, server_routes &)` serves the caller's `server_routes`** (`jllama_context::routes`, the one the model load builds) **instead of constructing one on its own frame.** `server_routes`' constructor registers a sleeping-state callback on the model's task queue, `server_queue::on_sleeping_state` only appends, so the frame-local object left a callback into freed memory behind when the attach returned, and the model's next idle sleep (`--sleep-idle-seconds`) ran it -- undefined behaviour, found at the b11361 bump and pinned by `NativeServerAttachSleepIntegrationTest` (model-backed, CI). Consequence: the route handlers read the **model's** params, so the endpoint toggles (`--metrics`, `--props`, `--slots`), `--slot-save-path`, the slot count and the sampling defaults are `ModelParameters` now (`enableMetricsEndpoint()` & co., added for that), and the attach argv configures the HTTP side only. |
+| `0007-server-attach-http-frontend.patch` | **Adds `llama_server_attach(argc, argv, server_context&)`** so the `NativeServer` *attach mode* can serve an **already-loaded `LlamaModel`** over the upstream HTTP frontend — no second model load, no `start_loop()`; the LlamaModel's worker keeps driving the shared `server_context` and the HTTP routes post tasks to its queue (the queue is the synchronization point). Mechanically: (1) extracts the **pure core route table** (`health` … `slots`) out of `llama_server()` into `static void llama_server_register_common_routes(ctx_http, routes)` (shared, so the two entry points cannot drift on the core endpoint set). **Scope note (narrowed at the b10154 bump):** the helper deliberately carries **only** the stable, state-independent route table — **not** the resumable-streaming routes (their handlers differ between router / non-router), the GCP-compat shim, or the experimental **CORS-proxy / MCP-server / built-in-tools** wiring. b10154 (upstream MCP-server support) moved the streaming routes into the middle of that block and coupled tools/CORS to a per-call `server_mcp mcp_mgr` lifecycle, so the earlier contiguous "route-table + CORS-proxy + tools" extraction is no longer possible; `llama_server()` keeps all of that inline, **byte-identical to upstream b10154** (only the route-table block is factored out). (2) adds `llama_server_attach`, which parses only the HTTP-side argv via `common_params_parse`, starts the stream-session GC + `server_http_context`, registers the common route table, the **non-router** resumable-streaming handlers (upstream b10154 paths `/v1/stream` GET/DEL + `/v1/streams/lookup` POST), the GCP-compat shim, and **403 "disabled" stubs for `/cors-proxy` + `/tools`** (attach mode does not wire the experimental CORS-proxy / MCP / built-in-tools host — those belong to a full `llama-server`, not an embedded model), marks ready immediately (model already loaded), and blocks on the HTTP thread until `llama_server_request_shutdown()` — never calling `common_init()`, backend init, `ctx_server.terminate()` or `llama_backend_free()` (the embedding caller owns those). Applies after `0001`+`0006` (same file); closes the "NativeServer — reuse an already-loaded LlamaModel" TODO. Upstream-submittable ("server: let embedding callers attach the HTTP frontend to an existing server_context"). **Refreshed at the b10519 bump:** upstream #26347 dropped the API key from the `/models` + `/v1/models` public-endpoint set and deleted the two trailing `// public endpoint (no API key check)` comments on those route registrations. Those two lines sit inside this patch's route-table removal block, so `git apply` failed ("patch does not apply", `server.cpp:258`) at **every** tag from b10519 on; the fix was to drop the now-wrong comment from all four affected lines (2 on the `-` side, 2 in the extracted helper on the `+` side), keeping the helper byte-identical to the block it replaces. **This is the invariant to re-check on every bump:** the `+` side of `llama_server_register_common_routes()` must stay a verbatim copy of the route table it factors out of `llama_server()`. **Refreshed at the b11104 bump** (upstream #28690, multi-address `--host`): `server_http_context` lost its single `thread` and `listening_address` members in favour of `join()` and a `listening_addresses` vector, one listener thread per bound address. The patch still *applied* cleanly there — only its own `+` lines named the removed members — so the applier could not see it; `llama_server_attach` now logs every address and blocks in `ctx_http.join()`, exactly as upstream's `llama_server()` does. **Refreshed at the b11361 bump** (#29818 adds `POST /v1/systemone` to the route table: one line, added on both sides so the helper stays verbatim) **and at b11401** (#29895 adds a `server_child &` overload of `llama_server()` beside the declarations and a `server_child child;` at the top of the argv entry point, the context of `0007`'s first two hunks; only context moved, the `+`/`-` lines are unchanged). **Changed in 5.2.x, not a refresh: `llama_server_attach(argc, argv, server_context &, server_routes &)` serves the caller's `server_routes`** (`jllama_context::routes`, the one the model load builds) **instead of constructing one on its own frame.** `server_routes`' constructor registers a sleeping-state callback on the model's task queue, `server_queue::on_sleeping_state` only appends, so the frame-local object left a callback into freed memory behind when the attach returned, and the model's next idle sleep (`--sleep-idle-seconds`) ran it -- undefined behaviour, found at the b11361 bump and pinned by `NativeServerAttachSleepIntegrationTest` (model-backed, CI). Consequence: the route handlers read the **model's** params, so the endpoint toggles (`--metrics`, `--props`, `--slots`), `--slot-save-path`, the slot count and the sampling defaults are `ModelParameters` now (`enableMetricsEndpoint()` & co., added for that), and the attach argv configures the HTTP side only. **Refreshed at the b11799 bump:** #26603 (`POST /tts`, b11781) and #30636 (the WebSocket `/v1/realtime`, b11780, registered with `ctx_http.ws()`, no `ex_wrapper`) joined the route table between the transcription and the Anthropic routes, so the removal block and the `+` side of `llama_server_register_common_routes()` both gained the two lines and attach mode serves them too; the helper stays the verbatim copy of the block it replaces, checked by script when the patch was regenerated. |
 | `0008-server-models-worker-cmd-override.patch` | **Makes router mode usable in-JVM.** The router (`server-models.cpp`) spawns each model worker by re-executing its own binary (`get_server_exec_path()` = `/proc/self/exe` & friends) — inside a JVM that binary is `java`, not a llama-server, so embedded router workers could never start. The patch adds env `LLAMA_SERVER_WORKER_CMD` (whitespace-split; read in `server_model_meta::update_args`) which replaces only the leading binary-path token of the rendered worker args, letting an embedding host relaunch workers through its own bootstrap — e.g. `java -cp app.jar net.ladenthin.llama.server.NativeServer` (each worker is then a fresh JVM running the classic single-model `NativeServer`). Exposed in Java as `NativeServer.setWorkerCommand(String...)` (JNI `setenv`); exercised by `RouterModeIntegrationTest` (Linux CI). Upstream-submittable (also useful for containerized/wrapped deployments). **Refreshed at the b11429 bump:** #29987 (untagged `4d60b4d08`, the commit before b11429) moved `server_model_meta::update_args` and changed the function after it (`update_caps(const common_params &)`), the hunk's trailing context; the `+` lines are unchanged. Since b11401 (#29895) a worker is a router *child* whose stdout carries the state commands, which is why `LlamaLoader` prints its two diagnostic lines to stderr (a JVM worker's early `System.out` line tripped the router until 2026-10-09, `e9758054`). |
 | `0006-server-embed-native-server-jni.patch` | **Makes `server.cpp`'s `llama_server` embeddable in the JVM** so the `NativeServer` JNI bridge can run the full upstream HTTP server (WebUI included) inside `libjllama` — see "Two server modes" below. b9870 already exposes `int llama_server(int, char**)` (non-static; no `main` in the file), so the patch only adds embedded-mode support: (1) a `g_llama_server_embedded` flag + `llama_server_set_embedded()` / `llama_server_request_shutdown()` (declared in the committed `src/main/cpp/native_server_bridge.h`); (2) skips installing the process-wide SIGINT/SIGTERM handlers when embedded (they would hijack the JVM's); (3) in embedded mode parses the **forwarded** argv via `common_params_parse` instead of `common_params_parse_main` (whose `GetCommandLineW` recovery would pick up `java.exe`'s command line — the same Windows class of bug `0001` fixes). `llama_server_request_shutdown()` mirrors the SIGTERM path (invokes the installed `shutdown_handler` → `ctx_server.terminate()` unblocks `start_loop()`), giving JNI an out-of-band stop since `ctx_server` is loop-local. **The handler is guarded, and both guards are load-bearing:** upstream's `shutdown_handler` lambdas capture `llama_server()`'s locals (`ctx_http`, `models_routes`, `mcp_mgr`, `ctx_server`) by reference and are never cleared, while `native_server.cpp` signals *repeatedly* until the worker reports it finished (a stop issued before the handler is installed, or before `start_loop()` begins, would otherwise be lost). A request landing after `llama_server()` returned therefore ran the lambda over destroyed objects — a `SIGSEGV` in `server_http_context::stop()` in `RouterModeIntegrationTest.tearDown` (run 36485159455), with router mode's `clean_up()` (unloading workers) making the window wide. The patch (1) takes `g_shutdown_handler_mutex` for every read, write and invocation of the handler (the caller is on another thread) and (2) declares a `llama_server_shutdown_handler_reset` **after** the captured locals, so the handler is cleared before any of them is destroyed; because invocation holds the same lock, that reset waits for an invocation in progress. `0007`'s `llama_server_attach` carries the same guard. Runnable guard: `src/test/cpp/test_native_server_shutdown.cpp`. Applies **after `0001`** (which flips this call site to `common_params_parse_main`), so its context is the post-`0001` tree; regenerate against `0001`+source on a bump. Only touches `tools/server/server.cpp`. |
 | `0014-common-log-callback-sink.patch` | **Gives `common_log` a callback sink: `common_log_set_callback(log, cb, user_data)` (`common/log.{h,cpp}`).** This is what `LlamaModel.setLogger` hooks. Before it, the Java logger was a `llama_log_set()` callback, which has two holes, both found while chasing `slot print_timing` lines interleaving with the Atmosphere agent's streamed answer: **(a)** every model load runs `common_init()`, which re-points `llama_log_set()` at `common_log_default_callback` (`common.cpp:394`), so `setLogger(…)` *before* `new LlamaModel(…)` silently lost the callback; **(b)** the server's own `SRV_*`/`SLT_*` macros are `LOG_INF` and write straight into `common_log`, which `llama_log_set()` never carried, so the per-request `slot …`/`srv …` lines could not be routed to Java at all (the reason `LlamaModelTest#testLogText/JSON` sat `@Disabled` for years). `common_log` upstream offers file, colors, prefix, timestamps, verbosity and JSONL but no hook. The patch adds one: while a callback is set, the worker thread hands every entry to it **instead of** printing to stdout/stderr (a `--log-file` still receives them); the callback gets the bare formatted message (no prefix/timestamp/colors) with the `ggml_log_callback` signature; swapping the callback pauses the worker first, so queued entries reach the *previous* sink (which is what makes `setLogger(format, null)` a synchronous drain). With the sink, `common_init()`'s `llama_log_set()` reset is harmless — it points at the default callback that feeds `common_log`, i.e. exactly the path into the sink — so the ordering problem (a) disappears without any re-install logic, and the `srv`/`slot` lines (b) arrive because they are `common_log` entries. `jllama.cpp`'s `setLogger` therefore sets `common_log_set_callback(common_log_main(), trampoline)` **plus** `llama_log_set(common_log_default_callback)` (so llama/ggml lines feed `common_log` even before the first load). Two consequences to know: the callback runs on `common_log`'s **worker thread**, a plain `std::thread` llama.cpp re-creates on every pause/resume and never attaches to the JVM — the trampoline attaches it **once per worker thread** as a daemon, named `jllama-log-worker`, and a `thread_local` guard detaches it when the thread exits (`get_jni_env_attaching`; `common_log::pause()` joins the worker, a normal exit on which TLS destructors run, and `JNI_OnUnload` pauses it before anything else). Until 5.2.x it attached and detached per line, which created a `java.lang.Thread` object per log line (13 lines of a failed load, 13 `Thread` objects -- `LlamaLoggerTest` pins the single attach now). Each call runs inside a `PushLocalFrame`/`PopLocalFrame` pair, because a thread that stays attached and has no Java frame would otherwise accumulate local references, `setLogger` must call `common_log_set_callback` **outside** `g_log_mutex`, because the pause joins the worker, which needs that mutex to read the callback, and `setLogger` callers are serialized by a **separate** `g_set_logger_mutex`: two unserialized swaps race on the worker's `std::thread` (one joins it while the other assigns a fresh thread over the still-joinable object = `std::terminate`, the whole JVM), which `LlamaLoggerTest#concurrentSetLoggerCallsDoNotRaceOnTheLogWorker` reproduced before the mutex existed. Two caveats the Javadoc carries: a caller must not hold a lock the *previous* callback needs (the drain runs it on the worker while the caller waits), and the verbosity threshold is process-wide and reset by **every** load (`common_params_parse` ends with `common_log_set_verbosity_thold(params.verbosity)`, default 3), so a load without `-lv` puts it back to 3 — a review assumed the opposite, and `LlamaLoggerTest#verbosityThresholdIsProcessWideAndEveryLoadSetsIt` now pins the measured behaviour. And the verbosity threshold applies *before* the sink: at the default (`3`) the Java logger sees errors, warnings and the server's INFO lines, while llama/ggml INFO lines (`common_log_get_verbosity` maps them to TRACE = 4) arrive only from `setLogVerbosity(4)` on — the same filtering the console gets, and a behaviour change for consumers who captured the unfiltered `llama_log_set()` stream before. **Runnable guards:** `src/test/cpp/test_common_log_callback.cpp` (6 tests over a private `common_log_init()` instance: delivery, bare text under prefix+timestamps, clear, swap-drains-to-old-sink, file kept, levels pass through) links the function on every platform, so a bump that drops the patch reds `C++ Tests` at link time; `LlamaLoggerTest` (model-free, needs only `libjllama`: a logger set before a deliberately failing load on a non-GGUF file sees the `srv … loading model` INFO line and llama's ERROR line, in TEXT and JSON) and the re-enabled `LlamaModelTest#testLogText/testLogJSON` plus `#testLoggerSetBeforeLoadSurvivesTheLoad` (vocab-only load) cover the Java side. Upstream-submittable ("common : add a callback sink to common_log for embedding hosts"); **not yet filed upstream**. Touches only `common/log.{h,cpp}`, which no other patch touches. **Refreshed at the b11401 bump** (#29895 adds a `colors` member next to the sink's members and a `get_colors()` next to `set_callback()`; only context moved, the `+`/`-` lines are unchanged, and upstream still has no hook -- it added `common_log_get_colors` only). **Refreshed at the b11612 bump** (`common_log_set_stdo()`, a stdout/stderr on-off switch stored as a `stdo` member right where the sink's members sit, and `print_entry()` now prints to the console only `if (stdo)`): three hunks of `common/log.cpp` lost their context and were regenerated; the sink keeps precedence -- `if (callback) { callback(...) } else if (stdo) { e.print(); }` -- so a sink still replaces the console and `set_stdo(false)` without a sink still silences it. Still no hook upstream (`common/log.h` names only `common_log_default_callback`). **On a bump, check whether upstream added a hook of its own (grep `callback` in `common/log.h`) and, if so, DROP this patch and port `setLogger` to theirs rather than refreshing it.** |
 | `0015-rpc-embeddable-client-and-stoppable-server.patch` | **Makes ggml-rpc usable inside a JVM** (see "RPC backend" below). Upstream treats every client-side RPC problem as `GGML_ABORT` — a malformed endpoint, a server that is not running, a failed handshake — which in a JVM kills the application over a typo in `--rpc`. **(1)** The *registration* path reports failure instead: `rpc_dispatcher::start()` returns `false`, `try_get_dispatcher()` `nullptr`, `ggml_backend_rpc_get_device_count()` `0`, `ggml_backend_rpc_add_server()` `nullptr`, and `add_rpc_devices()` (`common/arg.cpp`) throws `std::invalid_argument` naming the server where it used to register `nullptr` and silently run without it. Every path *after* registration keeps the original contract through `get_dispatcher()`, which still aborts — a server that vanishes mid-inference stays fatal (TODO). **(2)** `add_server()`'s cache hit is re-checked (a stopped server used to pass registration and abort on the first tensor upload), and `ggml_backend_rpc_get_device_memory()` of a gone server returns 0/0 instead of aborting — RPC devices stay in ggml's process-wide registry forever, and `common_init()` queries the memory of **every** registered device at `-lv 4`, so a stopped server would otherwise take down an unrelated later load. **(3)** The server becomes stoppable: `ggml_backend_rpc_stop_server()` (disconnects the client being served, wakes `accept()` with a loopback connection — the one portable way) and `ggml_backend_rpc_server_listening()`; the loop now reaches its cleanup and frees its backends on every early return, but deliberately **not** `rpc_transport_shutdown()`, whose `WSACleanup()` would invalidate every other RPC socket in the process. **(4)** transport: `MSG_NOSIGNAL`/`SO_NOSIGPIPE`, `socket_t::shutdown()`, and the fds `connect()`/`create_server()`/`accept()` leaked on their error paths are closed. Touches `ggml/include/ggml-rpc.h`, `ggml/src/ggml-rpc/{ggml-rpc.cpp,transport.cpp,transport.h}` and one function in `common/arg.cpp` (`add_rpc_devices`, away from `0001`'s `common_params_parse` hunks). **Runnable guard: `src/test/cpp/test_rpc.cpp`** — links both new functions (a bump that drops the patch fails `C++ Tests` at link time on every platform) and drives registration, stop and the memory query over loopback. Upstream-submittable; **not yet filed upstream**. On a bump, check whether upstream grew its own stop function or non-aborting registration (grep `stop_server` / `GGML_ABORT("Failed to connect` in `ggml-rpc.cpp`). **Refreshed at the b11450 bump** (#26610, `-sm tensor` over RPC, ~800 lines in `ggml-rpc.cpp`, protocol 7 → 8): three hunks lost their context -- new dispatcher members beside `start()`, and new `get_proc_address` names before `return NULL` (the two new names now sit after `ggml_backend_comm_allreduce_tensor`); the `+`/`-` lines are unchanged. The server-to-server comm #26610 adds (a listener on `0.0.0.0`, an `accept()` a stop does not wake) is on file in `TODO.md`. **Refreshed at the b11558 bump** (#29544 turns `GGML_RPC_DEBUG` into a verbosity level 0-3: one `ggml-rpc/log.h` with `LOG_ERROR`/`LOG_WARN`/`LOG_INFO` and `LOG_DBG`/`LOG_DBG2`/`LOG_DBG3`, every `GGML_LOG_*`, `printf` and `fprintf(stderr)` of the backend routed through it, and `transport_name()` where `shutdown()` goes in `transport.h`): 12 of the 24 hunks lost their context and were regenerated against the new text; the changes are the same, and the patch's own five error lines (the HELLO handshake in `negotiate_hello`, four in `rpc_dispatcher::start()`) use `LOG_ERROR` now like the code around them (`LOG_ERROR` is `GGML_LOG_ERROR`). One consequence outside the patch: the server's `Accepted client connection` line is `LOG_INFO` through ggml's log callback now (stderr by default, a `setLogger` sink at verbosity 4), no longer `printf` to stdout; `smoke-rpc.sh` captures both streams into `rpc-server.log`, so its grep is unaffected. |
 | `0017-ggml-cpu-x86-prefetch-byte-offset-and-msvc-type.patch` | **Makes `GGML_CPU_ALL_VARIANTS=ON` buildable with plain clang on Windows, and fixes a prefetch address that is wrong everywhere.** The four `_mm_prefetch` calls in `ggml/src/ggml-cpu/arch/x86/quants.c` (the SSSE3-without-AVX branch of `ggml_vec_dot_q4_0_q8_0`, and the only `_mm_prefetch` calls in the whole ggml tree) carry two independent defects; both are fixed by routing the address through one new `static inline` helper in the new header `ggml/src/ggml-cpu/arch/x86/prefetch.h`, which is included relatively, so **no CMake change is needed**. **(1) Type -- fatal only on a current clang:** MSVC declares the intrinsic as `void _mm_prefetch(char const *, int)`; clang's own `xmmintrin.h` defines a casting macro but only `#ifndef _MSC_VER`, so on a `*-windows-msvc` target clang leaves the declaration to Microsoft and a typed pointer is `-Wincompatible-pointer-types`. **clang 22 promoted that to an error by default** ([llvm-project #157364](https://github.com/llvm/llvm-project/pull/157364); the [22.1.0 release notes](https://releases.llvm.org/22.1.0/tools/clang/docs/ReleaseNotes.html) add that `-Wno-error=incompatible-pointer-types` downgrades it) -- *not* clang 16, which promoted only `-Wincompatible-FUNCTION-pointer-types`; an earlier version of this row said otherwise. Measured on the real file at `-msse4.2`, unpatched → patched: clang **20.1.8** 4 warnings → 0/0, clang **22.1.3** 4 errors → 0/0, clang **23.1.3** 4 errors → 0/0. Plain clang is exactly what `cmake/x64-windows-llvm.cmake` selects -- and plain clang is the only way to get all **14** CPU variants, because `clang-cl` makes CMake's `MSVC` true and the `if (NOT MSVC)` block in `ggml/src/CMakeLists.txt` then drops `zen4`, `cooperlake`, `sapphirerapids`, `ivybridge` and `piledriver`. **(2) Arithmetic:** `&x[ib] + sizeof(block_q4_0)` adds `sizeof()` *elements*, not bytes -- +324 and +1156 instead of +18 and +34 (the sizes `ggml-common.h` pins by `static_assert`), and +648 instead of +36 for the `2 *` pair. That half is wrong on every platform, including where the call compiles. **Measured:** no throughput difference (see the test-table row), so this is a correctness and portability fix, not a performance one. **Upstream-submittable and meant to be submitted.** **Why upstream's own build is green is measured, not an open question:** their `windows-cpu` release job builds exactly this with plain clang, `GGML_CPU_ALL_VARIANTS=ON` and no suppressing flag (`CMAKE_ARGS` carries only build-target toggles), and ships `ggml-cpu-sse42.dll` -- because the `windows-2025-vs2026` image has **LLVM 20.1.8** on `PATH`, where these are warnings. That is narrower than it looks: the same image also ships **clang 22.1.3** inside Visual Studio 2026 (`VC/Tools/Llvm/x64`), which *fails* on the unpatched file -- upstream reaches the working compiler only because `C:\Program Files/LLVM/bin` comes first on `PATH`. It is **not** a header difference: all three MSVC toolsets on that image (14.29.30133, 14.44.35207, 14.51.36231) declare it identically as `extern void _mm_prefetch(char const*_A, int _Sel);` -- that was the first hypothesis, and a one-off probe workflow (deleted again once it had answered) disproved it by printing the declaration from each toolset on the image. So this patch is **ahead of** upstream rather than diverging: their build breaks the moment the image ships a clang that makes the diagnostic fatal, and until then they compile four warnings and a prefetch address 18x/34x too far. **Runnable guard: `src/test/cpp/test_prefetch.cpp`.** On a bump, run `git grep -n _mm_prefetch ggml/src/ggml-cpu/arch/x86/quants.c` at the new tag -- a **locator, not a verdict**: it returns **four** hits at every tag, because the calls exist either way (b11639: lines 846, 847, 864, 865). Read them; the fix is upstream's only when the cast to a byte pointer happens **before** the addition, since `(const char *)(&x[ib] + sizeof(...))` casts the whole expression, silences the compiler and keeps the wrong address. Only then **drop this patch rather than refreshing it**. |
-| `0016-model-kolibri1.patch` | **Adds Aleph Alpha's Kolibri-1 (architecture `kolibri1`) ahead of upstream** -- a 78B German/English reasoning MoE (3.46B active) that upstream does not support yet ([ggml-org/llama.cpp#29922](https://github.com/ggml-org/llama.cpp/issues/29922), no PR at the time). A **temporary carry, not a fix**: drop it the moment upstream registers the architecture (`git grep -n kolibri src/llama-arch.cpp` at the new tag), and keep `test_kolibri1.cpp` -- its numerical comparisons must then pass against upstream's implementation, while its GGUF-format rows follow whatever format upstream's converter fixes (see the test-table row). **Architecture** (Aleph Alpha's vLLM reference, `aleph_alpha_inference/kolibri1.py` at `049a6a7`): GQA with per-head q/k RMSNorm; `layer_types` interleaves sliding-window layers (NEOX RoPE) with full-attention layers that have **no** positional encoding; sandwich norms around attention and MoE; every layer routed MoE + one ungated shared expert; the router selects top-k on **`logits + expert_bias`** and weights by the **unbiased `sigmoid(logits)`** -- not DeepSeek-V3's sigmoid router (selection on `sigmoid(logits) + bias`), which picks other experts as soon as the bias is non-zero. The graph therefore builds the selection itself and passes it to `build_moe_ffn` (`selected_experts_in`), leaving upstream's shared MoE code untouched. **Derived from two community ports, which are incompatible with each other's GGUFs** -- Eliasfpv28's AFMoE-based one (base b11378, `expert_gating_func` 2, pre-tokenizer `qwen2`) and the Qwen3-MoE-based one behind Hob-forge's GGUFs (base b11381, as carried in [mjwsolo/localcode#101](https://github.com/mjwsolo/localcode/pull/101): gating 5, pre-tokenizer `kolibri1`, rejects gating 2). This patch **loads both dialects**: gating 2, 5 or absent (all mean the one router), `kolibri1` mapped to the Qwen2 pre-tokenizer in `llama-vocab.cpp`, `expert_weights_norm` / `expert_shared_count` / `output.weight` optional. Every source, with license, is listed in the patch header; `REUSE.toml` annotates the file `MIT AND Apache-2.0` (Eliasfpv28's additions are Apache-2.0). Touches `src/llama-arch.{h,cpp}`, `src/llama-model.cpp` (mapping + NEOX rope list), `src/llama-vocab.cpp` (one name), `src/models/models.h` and the new `src/models/kolibri1.cpp`; no converter. **Runnable guard: `src/test/cpp/test_kolibri1.cpp`** (writes tiny random GGUFs in both dialects and compares every logit with an independent double-precision reference). **Verified against the real 78B model on 2026-10-11** (rented 16-vCPU / 123 GB instance, both published dialects fetched on the machine): both load, and a chat turn through the model's own template answers correctly in each -- `content` `"Berlin."` for a German question and `"51"` for 17x3, with the chain of thought in `reasoning_content` and `finish_reason` `stop`. **The two dialects tokenize identically** (`[452 22090 493 1678 2459 5513 46]`, round-tripping to the input sentence), which is the direct evidence that mapping `kolibri1` to the Qwen2 pre-tokenizer is right. Q4_K_M (44 GB) gave 13.4 t/s and Q3_K_S (31 GB) 14.6 t/s of generation on 16 cores. **Two traps for whoever repeats this:** a *raw* completion on this instruct model degenerates into repetition whatever the patch does, so measure through the chat endpoint; and `chatCompleteText` returns only `content`, which is **empty** when a reasoning model spends its token budget inside the thinking block -- use `handleChatCompletions` and a budget in the hundreds. **Still not verified:** GPU backends -- the community ports report real-model checks (top-1 67/67 against a float64 reference for Q8_0, tool calls, German text). |
 | `0018-ggml-amx-free-the-aligned-buffer.patch` | **The AMX weight buffer is freed with `free()` but allocated with `_aligned_malloc()` on Windows -- `STATUS_HEAP_CORRUPTION` (0xC0000374) the moment a model is unloaded.** `ggml_backend_amx_buffer_type_alloc_buffer()` (`ggml/src/ggml-cpu/amx/amx.cpp`) allocates with `ggml_aligned_malloc()`, which is `_aligned_malloc()` under `_MSC_VER` (plain clang on a `*-windows-msvc` target defines it too); `ggml_backend_amx_buffer_free_buffer()` called `free()` on it. Fine on Linux (`posix_memalign` + `free`), fatal on Windows, and only there. The patch frees with `ggml_aligned_free(buffer->context, buffer->size)`, exactly what `ggml_backend_cpu_buffer_free_buffer()` does; on Linux that is `free()`, so nothing changes there. **Reached by** the `sapphirerapids` CPU module (the only variant built with `AMX_TILE AMX_INT8`, which `ggml_backend_load_best` picks on Sapphire/Emerald/Granite Rapids; `ggml_amx_init()` is `return true` on `_WIN32`) and a model whose weights have AMX kernels (Q4_0, Q4_1, Q8_0, Q4_K, Q5_K, Q6_K, IQ4_XS -- i.e. every CI model). **Found by the second full run of the modular natives** (38069595101): `Java Tests Windows 2025 x86_64 (clang)` landed on an `Intel(R) Xeon(R) 6973P-C` runner (Granite Rapids), loaded `ggml-cpu-sapphirerapids.dll`, ran `GrammarExample`'s two generations fine and died with exit code -1073740940 at `LlamaModel.close()`; the first run (38065017595) had the same job green on an AMD EPYC 7763 (no AVX-512, `haswell`). **The Windows runners are a CPU lottery**, so which CPU module a Windows job exercises differs per run; before the modular natives the Windows jar was a static Haswell-level build and this code never ran. **Of the six callers of `ggml_aligned_malloc` in the tree this was the only mismatched pair** (`ggml-backend.cpp`, `ggml-cpu.c` twice, `ggml-openvino.cpp`, `ggml-zdnn.cpp` and `ggml.c` all pair correctly), and `ggml-backend.cpp:2464` is the line the fix copies. **Reproduced deliberately rather than waiting for the lottery again**: on a rented Granite Rapids machine, one CPU module staged at a time with the same model and the DLLs CI itself built, `sapphirerapids` crashed **6 of 6** runs -- including at **one** thread, which rules out a data race -- while `cooperlake`, `icelake` and `haswell` were clean 6 of 6, and on Linux with the same CPU all six exit 0 with byte-identical output. The crash lands *after* the inference prints its timings, in cleanup: the signature of a heap corrupted earlier and detected at `free`. With the patch applied the same matrix on the same machine is **clean in all twelve runs**. **The guard is a configure-time check, not a `jllama_test` case**: the AMX code exists only in that one variant of a `GGML_CPU_ALL_VARIANTS` build and executes only on AMX hardware, which the static test build, the local agent's Zen 3 and the cloud sandboxes lack -- so **`llama/cmake/check-aligned-free-pairing.cmake`** fails the configure on **every** platform when a ggml source calls `ggml_aligned_malloc` and never `ggml_aligned_free` (verified red against the unpatched source, naming `amx.cpp`, and green with this patch). A run-time test cannot replace it: without an AMX CPU there is nothing to observe, which is exactly how this shipped. What exercises it: the Windows x86-64 Java test job and the `windows-x86-64` smoke row whenever GitHub hands them an Intel AMX runner; the patch itself is held by `check-patches.py`, applies at b11639, and the patched file compiles with the variant's flags (GCC 13, `-mamx-tile -mamx-int8 -mavx512vnni ...`). Upstream master still carried the `free()` on 2026-10-10; **not yet filed upstream** (nothing before the release). **Drop-check on every bump, note the inversion:** `grep -n "free(buffer->context)" ggml/src/ggml-cpu/amx/amx.cpp` at the new tag -- a **hit** means the bare `free()` is still there and the patch is still needed; **gone** means upstream fixed it, drop the patch. Measured at b11639 (`a0e6dc738`): one hit, line 46. |
-| `0019-ggml-cpu-select-the-neon-fp32-to-fp16-conversion.patch` | **Every ARM build converts fp32 to fp16 in software, ours and upstream's.** The `__ARM_NEON` branch of `simd-mappings.h` defines both `GGML_CPU_COMPUTE_*` macros and selects only `FP16_TO_FP32`, so `GGML_CPU_FP32_TO_FP16` falls through to `GGML_COMPUTE_FP32_TO_FP16` -- ggml-base's deliberately portable software bit manipulation -- while `neon_compute_fp32_to_fp16`, one `fcvt`, stays defined and referenced by nothing. The `GGML_CPU_*` pair exists to be the *accelerated* variant; on ARM it was defined to be the unaccelerated one. `__POWER9_VECTOR__` and `__riscv_zfhmin` select both, and the comment on that fallback has always promised NEON does too. **Not a regression** -- absent in all 25 most recent commits touching the file, back to 2025-04-10. **Why only ARM feels it:** `ggml_cpu_fp32_to_fp16`, the `from_float` of `GGML_TYPE_F16`, has vector paths for F16C, RISC-V and s390x (the last from #30157) and **none** for NEON, so there the whole function is the scalar loop through the macro. **In the shipped artifacts:** no `fcvt` of any form in that function in any of this project's 17 ARM libraries (`cpu-linux-aarch64` 8 variants, `cpu-android-aarch64` 7, `cpu-windows-aarch64`, `metal-macos-aarch64`) nor in upstream's own `bin-ubuntu-arm64` / `bin-macos-arm64`; a plain `gcc` with no flags defines `__ARM_NEON` and `__ARM_FP16_FORMAT_IEEE` on aarch64, so nothing can miss it. **Measured 2.45x-13.3x** on five ARM core types, three operating systems and nine compiler builds, with the opposite direction in the same run as a control (within 3.2 % everywhere); **bit-exact over all 2^32 float inputs**, differing only in NaN payload, where x86 F16C gives the same numbers -- so it makes ARM agree with what x86 already ships. **Not an inference speed-up:** `perf` puts 83.9 % of an F16 `pp512` run in tinyBLAS's gemm, which converts in its own microkernel, and 0.03 % here. The patch also replaces the stale comment above the fallback with an `#error` pair that refuses a branch defining a COMPUTE macro without selecting it -- its position chosen by a mutation measurement, not by taste (4 of 4 correctness cases caught before the fallback against 3 of 4 directly after the chain). **The `FP16_TO_FP32` half carries `&& !defined(__F16C__)`, and that carve-out is load-bearing since the b11639 bump:** upstream #30343 (`685f70894` = b11635, merged 2026-10-14) puts that one direction back on the lookup table deliberately -- clang lowers the F16C intrinsic into the live accumulator register of the quantized dot products and serialises the loop, 45-65 % slower Q8_0/Q4_0 `MUL_MAT_ID` -- so without it the guard fires on **11 of the 14 x86-64 CPU variant modules**. Up to b11632 #30343 was absent, the `__F16C__` block still selected both directions and the carve-out was inert; from b11639 on the pin carries #30343, so x86 FP16->FP32 is on the lookup table exactly as upstream ships it, and the carve-out is what keeps the guard quiet there (`gcc -E` of the patched b11639 header silent for `-march=x86-64`, `-msse4.2`, `-march=haswell`, `-march=sapphirerapids`, `-march=znver4` and `-mavx -mf16c`). A **first** version of this patch also moved #30270's two F16C selections into the `__F16C__` branch; that was dropped when #30343 turned the block into a documented exception with an issue link, which is also why this diff now applies **both** at the pin and on upstream master (verified with `git apply --check` against each, and `clang -E` silent in all five configurations of the patched pinned source). Full record: [`docs/benchmarks/arm-fp16-conversion.md`](docs/benchmarks/arm-fp16-conversion.md). **Submitted upstream as [ggml-org/llama.cpp#30505](https://github.com/ggml-org/llama.cpp/pull/30505)** (open, rebased onto master 2026-10-14 at the maintainer's request), byte-identical to this diff -- the diff body of the patch file and `git diff` of the PR commit hash the same. **Drop-check on every bump, and its output must be read rather than tested:** `git show :ggml/src/ggml-cpu/simd-mappings.h \| sed -n '/^#if defined(__ARM_NEON)/,/^#elif/p' \| grep -n GGML_CPU_FP32_TO_FP16` prints **nothing** while the patch is needed and one line once upstream merged it -- then **drop this patch rather than refreshing it**. **Do not reduce the looser `git grep -n "GGML_CPU_FP32_TO_FP16(x) GGML_CPU_COMPUTE_FP32_TO_FP16"` over that file to a true/false:** it returns **three** hits at every tag so far (`__POWER9_VECTOR__`, `__riscv_zfhmin` and the out-of-chain `__F16C__` block, the first two with the NEON line's own four-space indent), so a `grep -q` reports "upstream fixed it" at every tag and deletes a fix that is still needed -- measured at b11639 (`a0e6dc738`): lines 71, 114 and 150, none in the `__ARM_NEON` branch, while `git apply --check` of the patch is clean. |
 | `0020-ggml-alloc-validate-leafs-before-reusing-an-allocation.patch` | **An `--embedding` model that is asked to generate crashes the process in `ggml_gallocr_alloc_graph`** (SIGSEGV on Linux, 0xC0000005 with both Windows toolchains), since upstream #30445 (`cb55ffe6e`, "graph: skip the LM head for pooled embeddings", b11574..b11612). `ggml_gallocr_alloc_graph()` assigns every tensor by **position** from the graph it was reserved with, and `ggml_gallocr_needs_realloc()` validates every node and every node's sources but of the **leafs only the count**. Since #30445 the LM head is expanded by `build_logits()` *after* `build_pooling()`, so an embeddings context reserves a graph that expands `t_embd` first and `t_logits` second, while a generation step on it (the server calls `llama_set_embeddings(ctx, false)` for a slot that samples) expands only `t_logits`, whose first source is `output.weight`: same node and leaf counts, every leaf after it shifted by one. An input leaf then lands on the slot of a weight (`buffer_id -1`) and, with the asserts compiled out, `ggml_vbuffer_tensor_alloc()` indexes `galloc->buffers[-1]`; a leaf landing on another input's slot is not size-checked either. The patch validates the leafs exactly as the nodes are validated, so an invalid leaf makes the allocator reserve again (one buffer) or return false, on which `ggml_backend_sched` reserves the graph itself. **Found by** Publish run 38648257387 (2026-10-14, the first full run since b11558): `ErrorHandlingTest` loads its model with `enableEmbedding()` and posts an infill request to it -- its first test in JUnit's order -- and the fork died in all three Java test jobs (`ggml_gallocr_alloc_graph <- ggml_backend_sched_alloc_graph <- llama_context::process_ubatch <- decode <- server_context_impl::update_slots`). **Reproduced locally without a downloaded model** (a random 2-layer llama GGUF with FIM tokens, context 128): infill crashes with `--embedding` and works without; a debug print showed leaf 31 `out_ids` arriving at a slot with no allocation; reverting `cb55ffe6e` makes it pass, and so does this patch -- with greedy sampling the `--embedding` model then returns the same tokens as the model without, run after run. Upstream master (`a0e6dc738`) still lacks the check; **not yet filed upstream**. **Runnable guard: `src/test/cpp/test_gallocr_leafs.cpp`** (two graphs with the same counts and the leafs swapped). **Drop-check on every bump:** read `ggml_gallocr_needs_realloc()` in `ggml/src/ggml-alloc.c` at the new tag -- once it walks `graph->leafs` itself, drop this patch rather than refreshing it. |
+| `0021-server-clear-the-slot-cache-when-the-server-adapters-change.patch` | **A slot's prompt cache survived a change of the LoRA adapters it was computed with.** `launch_slot_with_task()` (`tools/server/server-context.cpp`) clears the cache when a request names its own adapters and they differ from the slot's (`lora_should_clear_cache`, which keeps it only for aLoRA), but a request that names none takes `params_base.lora_adapters` in the `else` branch without that check. So the cache survived `SET_LORA` (`LlamaModel.setLoraAdapter(s)`, upstream's `POST /lora-adapters`) and the return to the server's adapters after a request with its own, and the slot answered like neither adapter set, silently. **Measured** (J9 of the bug-hunt brief, b11639, upstream's own LoRA test pair `stories15M_MOE-F16.gguf` + `moe_shakespeare15M.gguf`, greedy, one slot): after `setLoraAdapter(0, 0f)` the warm slot answered ". He saw a picture of a bird ..." where an emptied slot with the same scale answers " was a little girl ..."; only `eraseSlot(0)` restored it (F67). The patch applies the per-request rule in the `else` branch too; equal sets keep the cache as before. **A second hunk starts every slot with the server's adapters in `init()`** (`slot.lora = params_base.lora_adapters;`): `slot.lora` used to start empty, so a never-used slot of a server started with `--lora` read as "computed under no adapters", and its first task logged `0 loras -> 1 loras` and cleared a prompt it already held -- a state restored into it (`SLOT_RESTORE`) or loaded from the host-memory prompt cache by `get_available_slot()`, both computed under the server's adapters (F240). Upstream has the same defect on its HTTP route, so the patch is upstream-shaped; **not yet filed upstream**. Not covered, and said in `setLoraAdapters`' Javadoc: the host-memory prompt cache (`--cache-ram`, on by default) stores slot states without their adapters. **Runnable guard: `src/test/cpp/test_lora_cache.cpp`** (a random llama model and a random LoRA adapter on K, V and the FFN output, written by the test; five cases). **Drop-check on every bump, one per hunk:** read the `else` branch of the "process per-request lora adapters" block in `launch_slot_with_task()` at the new tag -- once it compares `params_base.lora_adapters` with `slot.lora` and clears the prompt (or `SET_LORA` clears the slots), drop that hunk rather than refreshing it; and read the slot loop of `init()` -- once it sets `slot.lora` from `params_base.lora_adapters`, drop that hunk. The patch goes when both have. |
 
-**Dropped patches** (`0004`, `0005`, `0009`, `0010`, `0011`, `0012`, `0013` -- upstream fixed each defect
-itself) are recorded in [`docs/history/dropped-llama-patches.md`](docs/history/dropped-llama-patches.md):
+**Dropped patches** (`0004`, `0005`, `0009`, `0010`, `0011`, `0012`, `0013`, `0016`, `0019` -- upstream fixed
+each defect itself, or for `0016` added the model; `0019` is this project's own fix, merged upstream as #30505) are recorded in [`docs/history/dropped-llama-patches.md`](docs/history/dropped-llama-patches.md):
 why each went, and the runnable guards that stayed behind re-pointed at upstream's contract. Read the
 matching note before reintroducing a patch for the same defect. **The standing drop-check on every
 bump:** the applier detects "does not apply", never "upstream already fixed this" -- for each
 remaining patch ask whether the *defect* is still there, not only whether the patch still applies.
 
-**And read each drop-check's output rather than its exit code — three of the four return hits at
-every tag.** Each row above now says what its own output means, because the generic form of these
+**And read each drop-check's output rather than its exit code — both remaining ones (`0017`, `0018`) return
+hits at every tag.** Each row above now says what its own output means, because the generic form of these
 greps is not a true/false and reducing one to a boolean has the worst possible failure mode: it
 deletes a fix that is still needed, silently, in the same commit that bumps the version. Measured at
-b11639: `0019`'s loose grep returns **3** hits (none in the branch it is about, while `git apply
---check` of the patch is clean — the two answers contradict each other and the grep is the one that
-is wrong), `0017`'s returns **4** at every tag because it locates calls that exist either way,
-`0018`'s is **inverted** (a hit means the defect stands), and only `0016`'s is a clean true/false.
+b11639, while `0019` was still carried: its loose grep returned **3** hits (none in the branch it was about, while
+`git apply --check` of the patch was clean — the two answers contradicted each other and the grep was the one that
+was wrong; the sed over the `__ARM_NEON` branch alone printed nothing until b11760 and one line at b11761, which is
+how that patch went, see `docs/history/dropped-llama-patches.md`), `0017`'s returns **4** at every tag because it
+locates calls that exist either way, `0018`'s is **inverted** (a hit means the defect stands); `0016`'s was the
+one clean true/false, and that patch went at the b11760 bump the way its row said -- upstream registered the
+architecture at b11709, the applier fired on the file it would have created.
 The same class cost a wrong answer once before, when a drop-check was run against a tag that did not
 exist yet: `git show` returned an empty file, the grep found nothing, and "upstream has not fixed it"
 was read out of a file that was never read. **A drop-check that cannot distinguish "no match" from
@@ -1641,7 +1653,13 @@ without `libjllama` and its whole Java-side lifecycle is tested with a fake back
 (`RpcServerLifecycleTest`) — the analysis build (SonarCloud, no native library) sees it covered, and a
 new lifecycle branch cannot hide behind "needs natives". The in-JVM `rpc-server`: ggml's blocking
 `ggml_backend_rpc_start_server()` runs on a daemon Java thread, `close()` calls
-`ggml_backend_rpc_stop_server()` (patch `0015`), and `start` waits for
+`ggml_backend_rpc_stop_server()` (patch `0015`) through `jllama_rpc_server_stop()` -- which keeps a
+stop issued during the serve call but before ggml binds the socket (ggml clears its own flag there)
+and returns once the serve has, so the FFM and C callers get what `RpcServer`'s Java retry gave the
+JNI path alone (F241); every serve call has an id, a stop is for the serves in progress when it is
+made and waits for those only, so a serve started while the stop still waits (a restart from
+another thread) is neither told to stop nor reached by the repeated ggml stop, which is issued under
+the registration mutex (F271) -- and `start` waits for
 `ggml_backend_rpc_server_listening()` so a bind failure is a `LlamaException` rather than a silently
 dead thread. It serves every accelerator, else the CPU — **never an RPC device**
 (`jllama::rpc::server_devices()`), which in a JVM that is also a client would forward to itself.
@@ -1659,8 +1677,10 @@ loading thread — the lock is reentrant, so a second complete load ran while th
 `System.load`, clearing the temp files and, with every GPU jar on the classpath, extracting every
 backend again over the library being loaded. The RPC smoke timed out on it (the doubled extraction
 of the CUDA/ROCm/SYCL libraries, then still complete libraries); it now waits 300 s like the other
-smoke and fails if the backend is selected more than once. Only the *nested* call is skipped — later calls still run the body,
-which `BackendLoadTest` relies on.
+smoke and fails if the backend is selected more than once. Since 6.0.0 `initialize()` is also idempotent
+after its first success (a changed `lib.path` or `backend` is named once on stderr and takes effect only
+in a new JVM; F264); `BackendLoadTest` drives repeated loads through the package-private
+`LlamaLoader.resetForTesting()`.
 **Single instance per process** (ggml keeps the server state in globals). `startLocal` binds
 loopback only; `startOnNetwork` is the explicit, warned opt-in, and binding needs an IPv4 literal
 (the server uses `inet_addr`). Endpoints on the client side are `value.RpcEndpoint`
@@ -1700,9 +1720,11 @@ tag and links `ssl`/`crypto` into its `cpp-httplib` target. **Android, s390x and
 are the exceptions** (`LLAMA_OPENSSL=OFF`, so `find_package` cannot link a system OpenSSL by
 accident): the dockcross cross-clang is not a configuration BoringSSL is built for here, big-endian
 is outside its supported set, and `icx`/`icpx` (compiler id `IntelLLVM`) is neither GCC nor Clang to
-BoringSSL's CMake -- upstream's `ubuntu-24-sycl` release job passes `LLAMA_OPENSSL=OFF` as well,
-while its `windows-sycl` job (C by `cl`, C++ by `icx`) builds BoringSSL, as the Windows SYCL job here
-did before. `verify-native-deps.py` is what holds the line: a `libssl` of any kind in a shipped
+BoringSSL's CMake -- upstream's `ubuntu-24-sycl` release job builds against the system OpenSSL instead since
+b11676 (#30456; `libssl-dev`, a dynamic `libssl` a shipped library here must not import), while its
+`windows-sycl` job (C by `cl`, C++ by `icx`) builds BoringSSL. The SYCL jars are module-only builds -- ggml
+alone, no cpp-httplib -- so no SSL code reaches them either way; the exception matters for a full Linux SYCL
+build of `libjllama` only. `verify-native-deps.py` is what holds the line: a `libssl` of any kind in a shipped
 library's dependency list fails `package`. The one import this adds is Windows' `crypt32.dll`
 (cpp-httplib's certificate-store lookup), an OS component since Windows 2000, in `WINDOWS_OS`.
 
@@ -1723,7 +1745,9 @@ sat in the link. The fix is one copy: `jllama` and `jllama_test` link upstream's
 (the two frameworks were already in the Mac allowlist), and on Linux BoringSSL's compiled-in defaults
 `/etc/ssl/certs` (hashed directory) and `/etc/ssl/cert.pem`, overridable with `SSL_CERT_FILE` /
 `SSL_CERT_DIR` -- which is also how a private CA or a corporate proxy's certificate is trusted.
-Upstream's download code has no `--insecure` and no proxy support; this project adds neither.
+Upstream's download code has no `--insecure`; since b11699 (#21752, `common/http.h`) its HTTP client honours the
+`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` environment variables (loopback never goes through the proxy). This project
+adds nothing on top.
 
 **Measured locally (Linux x86-64, the sandbox, BoringSSL `0.20260929.0`)** before the CI run: the
 same `NEEDED` list as before (no `libssl`, no `libcrypto`), 56 `httplib::SSLClient` symbols linked,
@@ -1782,10 +1806,13 @@ See `docs/history/llama-cpp-breaking-changes.md`'s `b10269–b10270` row for the
 this was a breaking **public API** change (`TextToSpeech`'s constructor and `synthesize()` overloads
 all changed shape) done deliberately — the project does not carry OuteTTS-compatibility shims.
 
-**Nothing to re-verify on a llama.cpp bump.** Because there is no generator or extracted header
-anymore, a version bump cannot silently break the TTS surface the way `patches/` or the old
-extraction could — `mtmd_helper::gen_audio`'s API surface is upstream's own committed public header,
-covered by the normal priority-8 API-compat review (`tools/mtmd/mtmd-helper.h` is on that list).
+**Nothing silent to re-verify on a llama.cpp bump, but the surface does move.** There is no generator or
+extracted header any more, so a bump cannot break the TTS surface the way `patches/` or the old extraction
+could: `mtmd_helper::gen_audio` is upstream's own committed public header, and a change there is a compile
+error in `tts_engine.cpp` (`tools/mtmd/mtmd-helper.h` is on the priority list). It moved at b11796 (#30902):
+the helper is default-constructed and initialised with `init(lctx, mctx)` / `valid()` instead of a
+constructor, and `mtmd_helper_gen_audio_inp_default()` fills the input struct, which gained a `stream` flag
+(off for a one-shot synthesis); `tts_engine.cpp` picked both up at the b11799 bump.
 
 ## Upgrading/Downgrading llama.cpp Version
 
@@ -2222,13 +2249,13 @@ If the local check passes (`BUILD SUCCESS`), the `mvn package` job in
 - `LlamaLoader` — Extracts the platform-specific native library from the JAR to a temp directory, or finds it on `java.library.path`.
 - `OSInfo` — Detects OS and architecture for library resolution.
 - **`server` package — OpenAI-compatible HTTP endpoint (a single implementation).**
-  - `server.OpenAiCompatServer` — built only on the JDK's `com.sun.net.httpserver` (no new dependency), embeddable and runnable via `java -cp  net.ladenthin.llama.server.OpenAiCompatServer …` (the classes jar's `Main-Class`, `ServerLauncher`, runs `NativeServer` by default — see "Two server modes"). Serves `POST /v1/chat/completions` (streaming via SSE + non-streaming), `POST /v1/completions`, `POST /v1/embeddings`, `POST /v1/rerank`, `POST /infill`, `GET /v1/models` and `GET /health` (every route is also reachable without the `/v1` prefix), so editors that speak the OpenAI protocol (e.g. VS Code Copilot "Custom Endpoint", Cline, Roo Code, Continue) can drive a local model. Streaming chat uses the native OAI chunk path (`LlamaModel.streamChatCompletion` → `requestChatCompletionStream` / `receiveChatCompletionChunk` + the C++ `wrap_stream_chunk` helper), preserving `delta.tool_calls`; completions/embeddings/infill forward verbatim to the matching `LlamaModel.handle*`; rerank reshapes `handleRerank` into the OAI `results`/`data` shape. The chat mapper forwards `stream_options` and `response_format` and defaults `cache_prompt=true`; a CORS `Filter` answers `OPTIONS` preflights; `OpenAiSseFormatter.ensureUsageCachedTokens` guarantees `usage.prompt_tokens_details.cached_tokens` on the streamed usage chunk (Copilot crash fix, microsoft/vscode #273482). **Agentic tool-calling is the primary target**; a C++ guard (`test_server.cpp`) pins `tool_calls.function.arguments` as a JSON string (llama.cpp #20198).
-  - **Alternative protocol surfaces** (pure translation over the OpenAI chat core — no second inference path; each reconstructs streamed tool calls via `ToolCallDeltaAccumulator`): **Ollama-native** (`GET /api/version`, `/api/tags`, `POST /api/show`, `/api/chat` with NDJSON streaming, `/api/generate` prompt-completion/FIM — `OllamaApiSupport`; `/api/show` advertises tools/insert/vision capabilities + context length for Copilot's Ollama provider), **Anthropic Messages** (`POST /v1/messages`, SSE event stream — `AnthropicApiSupport` + `AnthropicStreamTranslator`), and **OpenAI Responses** (`POST /v1/responses`, SSE event stream — `ResponsesApiSupport` + `ResponsesStreamTranslator`). The llama.cpp-native `GET /props` (context length + `modalities`) is served via `OpenAiSseFormatter.propsJson` for autocomplete clients that size their context from it.
-  - Supporting classes: `OpenAiServerConfig` (builder; optional bearer auth; binds `127.0.0.1`; `corsAllowOrigin`; `supportsVision`), `OpenAiServerCli` (testable CLI arg parser → `ModelParameters` + `OpenAiServerConfig`; flags incl. `--mmproj`/`-mmdev,--mmproj-device`/`--embedding`/`--reranking`), `OpenAiRequestMapper` (OAI chat request → `InferenceParameters`), `OpenAiSseFormatter` (SSE/models/error JSON + usage normalization), `OaiRerankSupport` (pure rerank request/response shaping), and the model-free test seam `OpenAiBackend`/`ChunkSink` + `LlamaModelBackend`. The streaming envelope is parsed by `json.ChatStreamChunkParser`.
+  - `server.OpenAiCompatServer` — built only on the JDK's `com.sun.net.httpserver` (no new dependency), embeddable and runnable via `java -cp  net.ladenthin.llama.server.OpenAiCompatServer …` (the classes jar's `Main-Class`, `ServerLauncher`, runs `NativeServer` by default — see "Two server modes"). Serves `POST /v1/chat/completions` (streaming via SSE + non-streaming), `POST /v1/completions`, `POST /v1/embeddings`, `POST /v1/rerank`, `POST /infill`, `GET /v1/models` and `GET /health` (every route is also reachable without the `/v1` prefix), so editors that speak the OpenAI protocol (e.g. VS Code Copilot "Custom Endpoint", Cline, Roo Code, Continue) can drive a local model. Streaming chat uses the native OAI chunk path (`LlamaModel.streamChatCompletion` → `requestChatCompletionStream` / `receiveChatCompletionChunk` + the C++ `wrap_stream_chunk` helper), preserving `delta.tool_calls`; completions/embeddings/infill forward verbatim to the matching `LlamaModel.handle*`, and a streamed `/v1/completions` verbatim to `LlamaModel.streamCompletionsOai` (upstream's own `text_completion` chunks, `usage` on the last); rerank reshapes `handleRerank` into the OAI `results`/`data` shape with `usage`, or, for a request with TEI's `texts`, into TEI's bare `[{index, score[, text]}]` array (`OaiRerankSupport.toResponse`, as upstream's `format_response_rerank`). The chat mapper forwards `stream_options` and `response_format` and defaults `cache_prompt=true`; a CORS `Filter` answers `OPTIONS` preflights; `OpenAiSseFormatter.ensureUsageCachedTokens` guarantees `usage.prompt_tokens_details.cached_tokens` on the streamed usage chunk (Copilot crash fix, microsoft/vscode #273482). **Agentic tool-calling is the primary target**; a C++ guard (`test_server.cpp`) pins `tool_calls.function.arguments` as a JSON string (llama.cpp #20198).
+  - **Alternative protocol surfaces** (pure translation over the OpenAI chat core — no second inference path; each reconstructs streamed tool calls via `ToolCallDeltaAccumulator`): **Ollama-native** (`GET /api/version`, `/api/tags`, `POST /api/show`, `/api/chat` with NDJSON streaming, `/api/generate` prompt-completion/FIM — `OllamaApiSupport`; `/api/show` advertises tools/insert/vision capabilities + context length for Copilot's Ollama provider), **Anthropic Messages** (`POST /v1/messages`, SSE event stream — `AnthropicApiSupport` + `AnthropicStreamTranslator`), and **OpenAI Responses** (`POST /v1/responses`, SSE event stream — `ResponsesApiSupport` + `ResponsesStreamTranslator`). Upstream's three token-count routes (`/v1/messages/count_tokens`, `/v1/responses/input_tokens`, `/v1/chat/completions/input_tokens`) translate like their generating route and count through `OpenAiBackend.countTokens` (template + tokenize; `501` for content only the projector can count). Every route answers its own path only (F112), and a failure after a stream started is the route's error event, not a silent end (F262). The llama.cpp-native `GET /props` (context length + `modalities`) is served via `OpenAiSseFormatter.propsJson` for autocomplete clients that size their context from it.
+  - Supporting classes: `OpenAiServerConfig` (builder; optional bearer auth; binds `127.0.0.1`; port 9931 by default, llama.cpp's, as `NativeServer`; `corsAllowOrigin`; `supportsVision`), `OpenAiServerCli` (testable CLI arg parser → `ModelParameters` + `OpenAiServerConfig`; flags incl. `--mmproj`/`-mmdev,--mmproj-device`/`--embedding`/`--reranking`), `OpenAiRequestMapper` (OAI chat request → `InferenceParameters`), `OpenAiSseFormatter` (SSE/models/error JSON + usage normalization), `OaiRerankSupport` (pure rerank request/response shaping), and the model-free test seam `OpenAiBackend`/`ChunkSink` + `LlamaModelBackend`. The streaming envelope is parsed by `json.ChatStreamChunkParser`.
   - The `server` package is a dedicated top layer in the ArchUnit `layeredArchitecture` rule (the only layer allowed to access the root `Api`); `noInternalJdkImports` carries an explicit exception for the supported `com.sun.net.httpserver` (the exported `jdk.httpserver` module, which `module-info.java` `requires`). See README "OpenAI-compatible HTTP server".
 
 **Native layer** (`src/main/cpp/`):
-- `jllama.cpp` — JNI implementation bridging Java calls to llama.cpp. 35 native methods (31 `LlamaModel` + 3 `TextToSpeech` + 1 `LlamaQuantizer`) plus `JNI_OnLoad`/`JNI_OnUnload`; since the C API (see "The C API" below) every one of them is a shell over `jllama_api.cpp` / `jllama_engine.cpp`, where the model logic lives.
+- `jllama.cpp` — JNI implementation bridging Java calls to llama.cpp. 36 native methods (32 `LlamaModel` + 3 `TextToSpeech` + 1 `LlamaQuantizer`) plus `JNI_OnLoad`/`JNI_OnUnload`; since the C API (see "The C API" below) every one of them is a shell over `jllama_api.cpp` / `jllama_engine.cpp`, where the model logic lives.
 - `utils.hpp` — Helper utilities (format helpers, argv stripping, token-piece serialisation).
 - `json_helpers.hpp` — Pure JSON transformation helpers (no JNI, no llama state). Independently unit-testable.
 - `jni_helpers.hpp` — JNI bridge helpers (handle management + server orchestration). Includes `json_helpers.hpp`.
@@ -2315,6 +2342,11 @@ Includes `json_helpers.hpp` so all bridge helpers can call transforms directly.
   specifies *Modified* UTF-8 input there, so standard UTF-8 containing supplementary-plane
   characters (every 4-byte emoji) is spec-invalid — Android CheckJNI aborts on it. The mirror of
   `parse_jstring`'s `String.getBytes("UTF-8")` input path.
+- **Messages go through `jni_utf8.hpp`** (`throw_utf8_impl`, `new_string_utf8_impl`): the same rule for
+  error text, which quotes paths and request content. ASCII stays on `ThrowNew`/`NewStringUTF`
+  (identical in both encodings), anything else crosses as bytes. Self-contained (jni.h only, no cached
+  ids), so `train_engine.cpp`, which never includes this header, uses it too. A new `ThrowNew` or
+  `NewStringUTF` with text that is not a literal goes through it.
 - `json_to_jstring_impl` — serialises any `json` value to a JNI string via upstream
   `safe_json_to_str` (dump with `error_handler_t::replace`, so content ending in an incomplete
   UTF-8 sequence yields U+FFFD instead of throwing `json::type_error 316`) + `utf8_to_jstring_impl`.
@@ -2349,8 +2381,8 @@ Functions with `_impl` suffix are called directly from `jllama.cpp`.
 
 An exception that escapes a native method and unwinds across the JNI boundary is **undefined
 behaviour and aborts the JVM** on most implementations. **Every `Java_*` entry point must therefore
-convert anything that escapes into a Java exception**, and there are 45 of them across four TUs —
-`jllama.cpp` (35), `native_server.cpp` (5), `rpc_bridge.cpp` (4), `train_engine.cpp` (1).
+convert anything that escapes into a Java exception**, and there are 46 of them across four TUs —
+`jllama.cpp` (36), `native_server.cpp` (5), `rpc_bridge.cpp` (4), `train_engine.cpp` (1).
 
 The mechanism is `jni_guard_impl(env, exception_class, [&]() -> Ret { … })` (`jni_helpers.hpp`,
 Layer A). It is **additive**: an entry point that already converts `std::exception` itself keeps
@@ -2569,6 +2601,24 @@ Locally the model tests self-skip when a GGUF is absent (`Assume.assumeTrue`), s
 checkout is fine.
 
 
+**Never compare two generations across a prompt-cache hit.** A second request with the same (or a
+prefix-equal) prompt and `cache_prompt` (the default) takes every prompt token but the last from the
+slot and re-evaluates that one token in a batch of its own (upstream `[TAG_PROMPT_LOGITS]`); its
+logits are not bit-identical to the batched prompt evaluation of the first request, and a greedy near
+tie flips -- measured on the random test model over 4000 prompts: the first token's logprob differs
+in 3999 first-vs-second pairs and the text in 4, while the second and the third request (both over
+the cached prefix) agree in all 4000 and two requests with `cache_prompt=false` are bit-identical.
+`ChatScenarioTest#testChatCompleteTextMatchesChatCompleteContent` was that lottery and lost once on
+the MSVC Windows job (run 39702748614, F272). So a test that needs two generations to agree either
+sets `withCachePrompt(false)` on both and proves it through the response's `timings` (`cache_n` 0,
+`prompt_n` equal to the prompt tokens), or compares the second with the third cached request and
+asserts the reuse (`cache_n == prompt_tokens - 1`, `prompt_n == 1`) -- never the first with the second.
+A sweep of every test tree the same day rewrote eight more (the ledger's F272 Nachtrag names them);
+the helpers to reuse are `MemoryManagementTest#assertCachedPrefixReused`,
+`ChatScenarioTest#assertWholePromptEvaluated` and `test_c_api.cpp`'s `without_prompt_cache` /
+`expect_whole_prompt_evaluated`.
+
+
 Test files are in `src/test/java/net/ladenthin/llama/` and `src/test/java/examples/`.
 
 ### C++ unit tests
@@ -2601,27 +2651,27 @@ ctest --test-dir build --output-on-failure -R "ResultsToJson"
 |------|-------|-------|
 | `src/test/cpp/test_utils.cpp` | 168 | Upstream helpers: `server_tokens`, `server_grammar_trigger`, `gen_tool_call_id`, `json_value`, `json_get_nested_values`, UTF-8 helpers, `format_response_rerank`, `format_embeddings_response_oaicompat`, `oaicompat_completion_params_parse`, `oaicompat_chat_params_parse`, `are_lora_equal`, `strip_flag_from_argv`, `token_piece_value`, `json_is_array_and_contains_numbers`, `format_oai_sse`, `format_oai_resp_sse`, `format_anthropic_sse`, `parse_lora_request`, `common_chat_parse` over malformed UTF-8 (the `ContentOnlyParseUtf8` guard, which pins upstream #29161's one-U+FFFD-per-invalid-run contract — formerly the guard for the dropped `patches/0011`) |
 | `src/test/cpp/test_server.cpp` | 206 | Upstream result types: `server_slot_stats` (the `timings` JSON payload; replaced `result_timings` in b10408), `task_params::to_json()` (incl. `dry_sequence_breakers`, `preserved_tokens`, `timings_per_token`), `completion_token_output`, `server_task_result_cmpl_partial` (non-oaicompat + `to_json_oaicompat` + logprobs + `to_json_oaicompat_chat` + `to_json_anthropic` + dispatcher), `server_task_result_cmpl_final` (non-oaicompat + `to_json_oaicompat` + `to_json_oaicompat_chat` + `to_json_oaicompat_chat_stream` + `to_json_anthropic` + `to_json_anthropic_stream` + tool_calls + dispatcher), `server_task_result_embd`, `server_task_result_rerank`, `server_task_result_metrics` (`to_metrics()` = the `/metrics` Prometheus exposition text; its `to_json()` has been unused since b10519 and returns `json{}` = JSON null), `server_task_result_slots` (`to_json()` = the `/slots` array, fed by the b10519 `SERVER_TASK_TYPE_SLOT_GET` task), `server_task_result_slot_save_load`, `server_task_result_slot_erase`, `server_task_result_apply_lora`, `server_task_result_get_lora`, `server_task_result_error`, `format_error_response`, `server_task::need_sampling()`, `server_task::n_tokens()`, `server_schema::eval_llama_cmpl_schema()` (parsing pipeline + grammar routing + error paths + per-request `dry_*` and `sse_ping_interval` field round-trips incl. hard-limit + server-default inheritance), `response_fields` projection |
-| `src/test/cpp/test_json_helpers.cpp` | 71 | All functions in `json_helpers.hpp`: `get_result_error_message`, `get_result_error_code` (the HTTP code of a failed task result, from which `jllama.cpp` picks `InvalidRequestException` or `LlamaException`), `results_to_json`, `rerank_results_to_json` (incl. missing/out-of-range `index` rejection), `parse_encoding_format`, `extract_embedding_prompt`, `is_infill_request`, `parse_slot_prompt_similarity`, `parse_positive_int_config`, `wrap_stream_chunk`, `server_metrics_to_json`, `route_error_message` |
+| `src/test/cpp/test_json_helpers.cpp` | 72 | All functions in `json_helpers.hpp`: `get_result_error_message`, `get_result_error_code` (the HTTP code of a failed task result, from which `jllama.cpp` picks `InvalidRequestException` or `LlamaException`), `results_to_json`, `rerank_results_to_json` (incl. missing/out-of-range `index` rejection and the `tokens_evaluated` pass-through), `parse_encoding_format`, `extract_embedding_prompt`, `is_infill_request`, `parse_slot_prompt_similarity`, `parse_positive_int_config`, `wrap_stream_chunk`, `server_metrics_to_json`, `route_error_message` |
 | `src/test/cpp/test_log_helpers.cpp` | 13 | All functions in `log_helpers.hpp`: `log_level_name`, `format_log_as_json` |
 | `src/test/cpp/test_common_log_callback.cpp` | 7 | **The runnable guard for `patches/0014`**: `common_log_set_callback()` on a private `common_log_init()` instance (never `common_log_main()`, so the process-wide logger the other tests print through is untouched) — delivery of level + bare text, no prefix/timestamp even when both are on (what `common_init()` does), clearing stops delivery, a swap drains queued entries to the *previous* sink (the property behind `LlamaModel.setLogger(format, null)` being a synchronous flush), a `--log-file` keeps being written alongside the sink, every `ggml_log_level` passes through unchanged, and the sink is not gated on upstream's `common_log_set_stdo()` console switch (b11612: entries reach the callback with stdout/stderr output off). The Java half (`LlamaLoggerTest`, model-free) proves the JNI trampoline on top of it. |
-| `src/test/cpp/test_jni_helpers.cpp` | 70 | All functions in `jni_helpers.hpp` using a zero-filled `JNINativeInterface_` mock (incl. the `utf8_to_jstring_impl` byte-array string path: emoji byte-preservation, truncated-UTF-8 replace-not-throw). Seven of them pin `jni_guard_impl` — the JNI exception boundary every `Java_*` entry point runs inside — including the `catch (...)` arm that is the only backstop for a non-`std::exception` type, and its two refusals (never `ThrowNew` over a pending Java exception, never with a null class). |
+| `src/test/cpp/test_jni_helpers.cpp` | 77 | All functions in `jni_helpers.hpp` using a zero-filled `JNINativeInterface_` mock (incl. the `utf8_to_jstring_impl` byte-array string path: emoji byte-preservation, truncated-UTF-8 replace-not-throw), and `jni_utf8.hpp` (six: an ASCII message stays on `ThrowNew`, a 4-byte character or an invalid byte reaches the exception constructor as the original bytes through `String(byte[], "UTF-8")`, the same for `new_string_utf8_impl` and for the JNI exception boundary; and seven: an exception already pending is left in place on both paths, as `jni_guard_impl` does). Seven of them pin `jni_guard_impl` — the JNI exception boundary every `Java_*` entry point runs inside — including the `catch (...)` arm that is the only backstop for a non-`std::exception` type, and its two refusals (never `ThrowNew` over a pending Java exception, never with a null class). |
 | `src/test/cpp/test_tts_wav.cpp` | 2 | The in-memory WAV writer `pcm_to_wav16_bytes` in `tts_wav.hpp` (WAV header/payload + little-endian clamping) — our own code, not upstream. The Qwen3-TTS pipeline it pairs with (`mtmd_helper::gen_audio`) is entirely upstream-owned (no project-side DSP to unit-test here). The load path is additionally covered by `test_tts_params.cpp` (3 tests over `tts_params.hpp`'s `build_tts_params`, plus 2 pinning the upstream `-1` default it depends on), which pins the CPU-thread resolution whose absence used to crash the JVM on every platform — see the `TODO.md` entry for the mechanism. End-to-end coverage is `TtsIntegrationTest`, which is model-gated. |
 | `src/test/cpp/test_tts_params.cpp` | 13 | The **three** builders every hand-assembled `common_params` goes through: `build_tts_params` (`tts_params.hpp`), `build_train_params` (`train_params.hpp`) and the shared `jllama::resolve_cpu_params` (`cpu_params.hpp`). Each builder is guarded separately on purpose — testing the resolver alone does **not** cover its call sites, because `train_engine.cpp` is compiled into `jllama` only, never into `jllama_test`, and `LlamaTrainerIntegrationTest` is gated on `net.ladenthin.llama.train.model`, which no CI job sets. Without these the JVM-abort bug could regress in the trainer on every platform, unseen. |
 | `src/test/cpp/test_prefetch.cpp` | 5 | **The runnable guard for `patches/0017`** (the x86 prefetch helper). The patch fixes two defects in the four `_mm_prefetch` calls of `ggml_vec_dot_q4_0_q8_0`'s SSSE3-without-AVX branch, and only one of them is a build error. The **type** half is caught by the compiler, but only by a recent one: plain clang on a `*-windows-msvc` target gets MSVC's `char const *` declaration (its own casting macro sits behind `#ifndef _MSC_VER`), and the resulting `-Wincompatible-pointer-types` is an **error by default from clang 22 on** -- measured on the real file at `-msse4.2`: 4 warnings on 20.1.8, 4 errors on 22.1.3 and 23.1.3, and 0/0 on all three once patched (`-msse4.2 -mavx` compiles either way, taking the AVX branch). So `GGML_CPU_ALL_VARIANTS=ON` stops building with plain clang on Windows once the compiler is new enough -- via the **`sse42`** variant, the only one that reaches `#elif defined(__SSSE3__)` (the `x64` baseline has no SSSE3 and takes the scalar branch; everything richer has AVX). Nothing in a test can cover that half. The **arithmetic** half is what this file guards, and it is silent: `&x[ib] + sizeof(block_q4_0)` adds `sizeof()` *elements*, so with the sizes `ggml-common.h` pins by `static_assert` (18 and 34) the prefetch targets +324 and +1156 bytes instead of +18 and +34, and the `+ 2 *` pair +648 instead of +36 -- wrong on **every** platform, including the ones where the call compiles. `ggml_prefetch_at()` casts to `const char *` first, which is what makes the offset byte-wise. **The case that makes the file worth having:** writing the cast around the whole expression, `(const char *)(&x[ib] + sizeof(block_q4_0))`, silences the compiler and keeps the wrong address -- that form builds and fails here. Verified both ways: 5/5 green against the patched header, 5/5 red against a deliberately element-wise one. Includes two internal headers (`ggml-common.h` for the block types, `prefetch.h` for the helper) via the `${llama.cpp_SOURCE_DIR}/ggml/src` and `.../ggml/src/ggml-cpu/arch/x86` include dirs added for it. **Measured, so the upstream report does not overclaim:** the wrong prefetch distance costs no measurable throughput (Ryzen 7 5800H forced onto the `sse42` kernel, 0.6B Q4_0: pp512 101.3 vs 100.9, tg128 37.3 vs 37.0, inside each other's error bars) -- it is a correctness and portability fix, not a performance one. |
-| `src/test/cpp/test_kolibri1.cpp` | 5 | **The runnable guard for `patches/0016`** (Kolibri-1). Writes tiny random `kolibri1` GGUFs with the public `gguf` API, loads them through the real library on the CPU and compares every logit -- one batch, and token by token through the iSWA KV cache -- with an independent double-precision reference written from Aleph Alpha's vLLM implementation, not from the patch. The layer pattern puts a full-attention (NoPE) layer between sliding ones, the sequence is longer than the window, and the expert biases are large enough that DeepSeek-V3's router would pick other experts (one test asserts that, so the comparison cannot pass vacuously). Covers both published GGUF dialects (gating 2 + `qwen2` + `output.weight`; gating 5 + `kolibri1` + tied output), renormalized routing, and the rejection of another gating function. **What happens when upstream supports Kolibri-1 and `0016` is dropped** -- the file compiles unchanged (public `llama.h`/`gguf.h`/`ggml.h` only, nothing from the patch), but its tests split in two. The **numerical comparison** against the reference must stay green, and `TheDeepSeekRouterWouldGiveDifferentLogits` never touches the library at all; a red comparison means upstream computes something else than Aleph Alpha's reference (router, window boundary, positional encoding) and is a finding, not a test to adjust. The **GGUF format** each test writes -- architecture name, key and tensor names, `expert_gating_func`, pre-tokenizer, optional `output.weight` -- is upstream's converter's decision, not ours: `AfmoeDialectMatchesTheReference` (gating 2) is the likeliest to go red if upstream follows the gating-5 port, `RenormalizedRoutingMatchesTheReference` writes no gating key at all, and `AnotherGatingFunctionIsRejected` assumes a rejection upstream need not make. A red format row is a real signal as well -- the published GGUFs of that dialect stop loading without the patch -- so decide it deliberately (a small compatibility patch, or document that those files must be reconverted) and only then move the row to upstream's format: one `{gating, pre, ...}` entry per test, with the reference untouched. |
+| `src/test/cpp/test_kolibri1.cpp` | 3 | **The guard for upstream's Kolibri-1 support** (b11709, #30673; carried as `patches/0016` until the b11760 bump, see `docs/history/dropped-llama-patches.md`). Writes tiny random `kolibri1` GGUFs with the public `gguf` API in the format upstream's converter writes (`conversion/kolibri1.py` over `AfmoeModel`: gating function 2, pre-tokenizer `qwen2`, `expert_weights_norm`, an explicit `output.weight`, the per-layer sliding-window pattern), loads them through the real library on the CPU and compares every logit -- one batch, and token by token through the iSWA KV cache -- with an independent double-precision reference written from Aleph Alpha's vLLM implementation, not from llama.cpp. The layer pattern puts a full-attention (NoPE) layer between sliding ones, the sequence is longer than the window, and the expert biases are large enough that DeepSeek-V3's router would pick other experts (one test asserts that, so the comparison cannot pass vacuously); the second comparison turns `norm_topk_prob` on. A red comparison means upstream computes something else than Aleph Alpha's reference (router, window boundary, positional encoding) and is a finding, not a test to adjust. The two format rows of the community dialects the patch loaded (gating 5 + pre `kolibri1` + tied output; the rejection of gating 1) went with the patch: upstream reads the keys above as required, knows no pre-tokenizer `kolibri1`, and passes an unknown gating value on to `build_moe_ffn` instead of refusing it -- those GGUFs are reconverted, not shimmed (README, "Kolibri-1"). |
 | `src/test/cpp/test_model_flags.cpp` | 4 | **The contract between the Java CLI-flag registries and llama.cpp's server argument parser.** CMake reads `ModelFlag.java` + `ModelOption.java` (`cmake/extract-java-wire-names.cmake` → a generated header of `{name, contract}` pairs), and this file asserts every `SERVER_PARSER` name is in `common_params_parser_init(params, LLAMA_EXAMPLE_SERVER).options`. It exists because **no Java test can catch this class**: `ModelFlagTest`/`ModelParametersExtendedTest` pin the *string mapping* (`hasKey("--mlock")`), never that llama.cpp still accepts the string, so they stay green forever while the flag is dead — and `common_params_parse` treats an unregistered option as a hard error, so the affected builder method makes the model **unloadable**, not merely ineffective. **A grep over `arg.cpp` is not a substitute**: `--grp-attn-n`/`-w` are present there at every pinned tag but `set_examples()`-scoped to `LLAMA_EXAMPLE_COMPLETION`/`PASSKEY`, so the server parser rejects them exactly like a deleted flag — only the real option table sees that. `--vocab-only` is the one exemption, and it declares itself `CliContract.PROJECT_PSEUDO` on its own constant rather than appearing in a list inside this file; the test asserts such a name is **still unknown** to the parser (an exemption upstream later registers would be hiding a real check) and that the exempt set is non-empty. |
 | `src/test/cpp/test_wire_contracts.cpp` | 6 | **The same contract for the two quieter surfaces.** `RequestField` against `server_schema::make_llama_cmpl_schema(...)` (5 tests) and `TrainingField` against `jllama_train::config_keys()` (1 test). Both receivers *silently ignore* an unknown key — the schema skips it, `train_engine.cpp` reads with `j.value(key, default)` and falls back — so a dead field produces no error anywhere and every string-mapping test keeps passing. `OAI_LAYER`-declared keys (consumed by `oaicompat_*_params_parse` before the schema) are exempt from the schema check, and are checked **both** ways: still unknown to the schema (the inverted check), and read by at least one upstream reader-shaped site (the configure-time sweep — this is what caught `chat_template`, a key a public builder wrote and nothing read). See [`docs/history/parameter-wire-surface.md`](docs/history/parameter-wire-surface.md). |
 | `src/test/cpp/test_rpc.cpp` | 28 | **The runnable guard for `patches/0015` and for `rpc_support.hpp`.** The device-selection rules (pure, literal inputs: `--rpc` endpoints accumulate across repeated options, an explicit `--device`/`-dev` is never overridden, stale RPC devices are replaced by the local GPUs chosen the way llama.cpp's default does — duplicates by device id dropped, iGPUs only without a discrete GPU, `none` on a CPU-only host). Then the **real** ggml-rpc client and server over loopback, no model: a `mul_mat` graph computed on the RPC backend equals the local CPU result; `ggml_backend_rpc_stop_server()` ends the server, frees the port and disconnects a client that is still connected; an unreachable or malformed endpoint is `nullptr`/`std::invalid_argument` instead of an abort; a registered server that went away is re-checked on the next registration; its device reports 0/0 memory instead of aborting; and a stale server is kept out of a later load that did not ask for it. The loopback server picks a free port from a range so parallel jobs do not collide, and serves the **CPU** by name — a served GPU that lacks an op aborts the server (see the RPC section). Also: server devices chosen by name (case-insensitive, deduplicated), an unknown name listing the available devices, and a registered RPC device refused; the mmproj device pinned the way clip would choose it minus the stale server; and `exclude_stale_devices()`, the params-level guard for TTS and the trainer. |
-
-| `src/test/cpp/test_c_api.cpp` | 27 | **The C API (`jllama_api.h`, layer B) without a JVM.** Writes a tiny random two-layer llama GGUF with the GPT-2 byte tokenizer (the public `gguf` API, like `test_kolibri1.cpp`), loads it through `jllama_model_load` and drives the shipped contract: version and build info, null-argument refusal with a message, a missing model failing to load, the load-progress callback (reported, and a `false` return aborts the load), a chat completion that is an OpenAI `chat.completion` object, an invalid request answered as the caller's fault (`JLLAMA_STATUS_INVALID_REQUEST`), streaming until `stop` with the task released afterwards ("Task not found" on the next receive), a released task gone, and the log callback seeing the server's lines with a null set flushing synchronously. Hermetic, so it runs in every `C++ Tests` job without `restore-models`; the output of a random model is noise, the contract is what is asserted. Two things the fixture does on purpose: the output rows of every non-ASCII and special token are scaled to almost nothing, because a byte above 0x7F is an incomplete UTF-8 piece the server holds back rather than streams (a random model that emits only those sends no partial chunk at all), and the task id is checked `>= 0` -- the server numbers its tasks from zero. Then every other `LlamaModel` operation in turn (the meta, raw and OAI completions, raw streaming, the template, tokenize/detokenize, metrics, slots, LoRA, configure, the refusals of a model loaded without `--embedding`/`--reranking`/FIM tokens, the grammar conversion, a Q8_0 quantisation that loads again), then the other engines: the embedded server in router mode and attached to the model (which still answers after the server stopped), the RPC server over loopback, text to speech refusing missing models, and a fine-tuning run over the random F32 model that writes a GGUF -- which is why `tts_engine.cpp`, `train_engine.cpp` and `native_server_engine.cpp` are in `jllama_test`'s source list. Four model-free cases (`CApiStatic`) run before any load. |
+| `src/test/cpp/test_c_api.cpp` | 51 | **The C API (`jllama_api.h`, layer B) without a JVM.** Writes a tiny random two-layer llama GGUF with the GPT-2 byte tokenizer (the public `gguf` API, like `test_kolibri1.cpp`), loads it through `jllama_model_load` and drives the shipped contract: version and build info, null-argument refusal with a message, a missing model failing to load, the load-progress callback (reported, and a `false` return aborts the load), a chat completion that is an OpenAI `chat.completion` object, an invalid request answered as the caller's fault (`JLLAMA_STATUS_INVALID_REQUEST`), streaming until `stop` with the task released afterwards ("Task not found" on the next receive), a released task gone, and the log callback seeing the server's lines with a null set flushing synchronously. Hermetic, so it runs in every `C++ Tests` job without `restore-models`; the output of a random model is noise, the contract is what is asserted. Two things the fixture does on purpose: the output rows of every non-ASCII and special token are scaled to almost nothing, because a byte above 0x7F is an incomplete UTF-8 piece the server holds back rather than streams (a random model that emits only those sends no partial chunk at all), and the task id is checked `>= 0` -- the server numbers its tasks from zero. Then every other `LlamaModel` operation in turn (the meta, raw and OAI completions, raw streaming, the streamed OAI completion (`text_completion` chunks with one id, probabilities, `usage` on the last), the template, tokenize/detokenize, metrics, slots, LoRA, configure, the refusals of a model loaded without `--embedding`/`--reranking`/FIM tokens, the grammar conversion, a Q8_0 quantisation that loads again, several prompts in one request and several completions of one (`"n"` > 1) refused on the blocking, raw and streamed paths while a token array stays one prompt, and every OpenAI answer and chunk carrying a `chatcmpl-` id and the model name, one id per stream), then the other engines: the embedded server in router mode and attached to the model (which still answers after the server stopped), the RPC server over loopback, text to speech refusing missing models, and a fine-tuning run over the random F32 model that writes a GGUF -- which is why `tts_engine.cpp`, `train_engine.cpp` and `native_server_engine.cpp` are in `jllama_test`'s source list. Four model-free cases (`CApiStatic`) run before any load. `CApiSlotFiles` loads the random model with `--slot-save-path`: a saved slot file lands under that directory by its name and restores, and a name with a directory part, `..`, an absolute path or nothing is `JLLAMA_STATUS_INVALID_REQUEST` with nothing written (red before F251: the name was the path, relative to the working directory); a model without the directory refuses save and restore with `JLLAMA_STATUS_ERROR`. Four `CApiRpc` cases pin `jllama_rpc_server_stop()` around the bind and the next serve: a stop issued before ggml binds the socket -- from the serving thread through the test seam `jllama::rpc::set_serve_hook_for_testing()` (`rpc_support.hpp`, not exported), or from another thread while the serve is held in that seam, where the stop must not return before the serve has -- ends the serve instead of being lost (patches/0015 clears ggml's stop flag at the bind; red before F241), a stop while nothing serves does not stop the next serve, and a stop does not reach a serve started while it waits for the one it is for (the stop seam `set_stop_hook_for_testing()` starts the next serve once the stop is recorded; red before F271: the stop waited 30 s for the newcomer and ended it too). Two `CApiFim` cases load the same random model with FIM tokens (`write_tiny_llama_gguf(prefix, true)`): a raw completion with `input_prefix`/`input_suffix`, blocking and streamed, evaluates exactly the prompt `jllama_infill` builds from the same fields, and one of the two fields alone is refused naming the other (red before F246: the completion path completed the empty prompt and dropped both). Two more `CApi` cases pin `messages` on the raw paths: blocking and streamed, the request evaluates exactly the tokens of a completion of the prompt `jllama_apply_template` renders from them (an empty `prompt` beside them counts as none), and a non-empty `prompt` or the infill fields beside them are refused (red before F247: the field was ignored). Two more: the chat fields the raw paths cannot apply beside `messages` (`response_format`, `tools`, `tool_choice`, `parallel_tool_calls`, `top_logprobs`, `thinking_budget_tokens`) are refused by name on both raw paths, and `CApiTemplateStops` loads the random model with an end-of-generation byte of its own (`write_tiny_llama_gguf(prefix, false, 4)`) and a template upstream adds the stop string `` to, which a grammar forces: the raw paths stop there as the chat path does, also beside a `stop` of the request (red before F248: dropped, `stop_type` `eos` and the string in `content`). And `CApiReasoningBudget` loads it with a template that opens a `` block in its generation prompt: a raw completion with `messages` and `reasoning_budget_tokens` 1 completes the forced `` and then the chat path's answer, blocking and streamed, while an output-format grammar beside it still runs (the generation prompt the budget is primed with is left out there); a template without thinking tags leaves the request unchanged (red before F249: the tags were dropped and the budget did nothing). A third case loads the random model with ``, `` and `` as control tokens (`write_tiny_llama_gguf(prefix, false, 4, special)`): the forced end tag reads as text in the raw content, blocking and streamed, and a request's own `preserved_tokens` stays in force beside the template's (red before F250: the session's preserved tokens were not taken over, and the special end tag was an empty piece). A fourth, without `messages`, names the tags in the request with a logit bias that makes the model write the start tag: a budget of zero forces the end tag after each one, and a `reasoning_budget_message` before it (red before F252: without the message the budget forced nothing). Four cases pin the raw multimodal prompt: `CApiVision` loads the random model with a random llava-style vision projector the shared fixture writes too (`write_tiny_mmproj_gguf()`, `--mmproj`) and finds the server's `media_marker` in the model meta; a raw completion whose `prompt` is upstream's `{"prompt_string": "......", "multimodal_data": [""]}` evaluates the text tokens plus the image's four patch embeddings on the blocking, streamed and OpenAI paths (red before F258: refused as if the model had no projector), while markers that do not match the images, data that is not base64, no data and an image at the very end (an abort of the process in `update_slots`) are refused, and the same object on the model without a projector is the caller's fault. `CApiVocabOnly` loads the random model with `--vocab-only`: the meta, tokenize and detokenize calls answer, and a parameterized case per other C function on a model (22 instances: every completion, chat and stream path, the template, embeddings, rerank, infill, metrics, slots, LoRA, configure, systemone, the attached server) is `JLLAMA_STATUS_ERROR` with "`jllama_` is not available in vocab-only mode" (red before F266: a `SIGSEGV` in `llama_vocab::token_bos()` or a wait that never ended). |
 | `src/test/cpp/test_native_server_shutdown.cpp` | 3 | **The runnable guard for the shutdown-handler guard in `patches/0006`/`0007`.** Runs the real `llama_server()` in router mode over an empty `--models-dir` on an ephemeral loopback port (no model, no worker) and stops it the way `native_server.cpp` does. Pins a clean stop (exit code 0), that `llama_server_request_shutdown()` **after** the server returned is a no-op (the deterministic form of the CI `SIGSEGV` — it crashed every run before the fix), and that requests hammered from another thread while the server tears down are safe (5 rounds). Compiled on non-Android only, like `server.cpp` itself. |
 | `src/test/cpp/test_gallocr_leafs.cpp` | 1 | **The runnable guard for `patches/0020`.** Two graphs with the same two nodes in the same order (`n1 = add(x, x)`, `n2 = mul(w, n1)`) and only the leafs moved: one expands `n1` before `n2` and meets the input first, the other expands only `n2`, whose first source is the weight -- the shape of the llama.cpp case, where every node and node source still matches its slot, so the old check passed it. Reserved with the first, the second must get an allocation for the input inside the allocator's buffer and compute the right product on it. No model, every platform. **Verified both ways:** with the fix removed from the source tree the test dies with SIGSEGV (rc 139); a first version that swapped the operands of one `add` passed without the fix, because a swapped *node source* was already caught -- a guard has to move only the leafs. |
+| `src/test/cpp/test_lora_cache.cpp` | 5 | **The runnable guard for `patches/0021`.** Writes a random llama model (the shared `tiny_llama_gguf.hpp`, also used by `test_c_api.cpp`) and a random LoRA adapter on K, V and the FFN output of every layer, loads them with one slot and `--cache-ram 0`, and answers the same prompt greedily. After `jllama_set_lora_adapters` (scale 1 -> 0 and back) and after a request with its own adapters, the warm slot must answer exactly like an emptied one. One case guards the others against passing vacuously: the adapter changes the answer, and a repeated request reuses the cache (`timings.cache_n > 0`; not `tokens_cached`, which counts every token the slot holds and is positive after any request). After each adapter change the next request must take nothing from the cache (`timings.cache_n == 0`), so those three cases are red without the patch whether or not the random model happens to answer differently. **Verified both ways:** without the patch 3 of 4 red (the warm slot answered from the old scales), with it 4 of 4 green in three runs; no model, every platform. A second fixture (`LoraCacheFreshSlot`: two slots, `--slot-save-path`) saves slot 0 (by its file name inside that directory) after a request and restores it into the never-used slot 1, whose request must take all of the prompt but at most two tokens from it and answer like slot 0 -- red before the `init()` hunk (`Expected: (prompt_n) <= (2), actual: 77 vs 2`), green with it (F240). |
 
-**Current total: 629 tests (all passing).**
+**Current total: 664 tests (all passing).**
 
 #### Upstream source location (in CMake build tree)
 
-llama.cpp is fetched via CMake FetchContent, pinned to `JLLAMA_GIT_TAG b11639`.
+llama.cpp is fetched via CMake FetchContent, pinned to `JLLAMA_GIT_TAG b11815`.
 
 **GoogleTest** is a separate `BUILD_TESTING`-only FetchContent (`GIT_TAG v1.18.0`), used solely
 by the `jllama_test` C++ unit-test binary — not by the shipped library, and not coupled to the
@@ -3093,12 +3143,20 @@ the "Java 22+ over the C API" item of the delivery roadmap (TODO.md item 10), de
   are major 66 and are not part of the smoke sets.
 - **Tests**: `JllamaNativeTest` (model-free: `apiVersion` equals the pom version surefire passes,
   `buildInfo` starts with `LlamaCppVersion`, a grammar conversion, a missing model's message, the log
-  callback reached from the worker and flushed on `null`, RPC device names) and
-  `FfmLlamaModelIntegrationTest` (the draft model, or `-Dnet.ladenthin.llama.ffm.model`: progress
-  callback, meta, chat completion, streaming to `stop` and an early close, raw completion and its
-  stream, tokenize/detokenize round trip, template, metrics, LoRA and configure, slot erase and the
-  refused save, the embedding refusal, invalid requests, the attached server answering `/health` and
-  the model outliving it, a closed model refusing calls). Locally the C++ test fixture's twin, a
+  callback reached from the worker and flushed on `null`, an `Error` thrown by the log callback losing
+  that line and nothing else, RPC device names, and three `takeStrings` cases: a populated array read
+  in order and freed with its count, an empty non-null array still freed, a null array not freed),
+  `FfmNativeServerTest` and `FfmTextToSpeechTest` (no native library, fake `Backend`s: `close()` waits
+  for an `isRunning()` / `synthesize` call inside the native call -- the lifecycle lock, a read/write
+  lock for TTS --, a call after `close()` does not touch the handle, a second `close()` has no
+  effect), `FfmRpcServerTest` (loopback, CPU: a stop while nothing serves does not stop the next serve,
+  a stop ends it) and `FfmLlamaModelIntegrationTest` (the draft model, or
+  `-Dnet.ladenthin.llama.ffm.model`: progress callback, meta, chat completion, streaming to `stop` and
+  an early close, raw completion and its stream, the OAI completion stream ending with `usage`,
+  tokenize/detokenize round trip, template, metrics, LoRA and configure, slot erase, the save refused
+  without `--slot-save-path` and a slot file named inside that directory (a path refused), the embedding refusal, invalid requests, the attached server answering `/health` and the model
+  outliving it, an `Error` thrown by the progress callback aborting the load instead of the JVM, a
+  closed model refusing calls). Locally the C++ test fixture's twin, a
   tiny random llama GGUF written with `gguf-py`, serves as the model where no download is possible.
 - **`versions.POMS`** names `llama-ffm/pom.xml` in all four repositories (a shared file).
 
@@ -3132,10 +3190,20 @@ certificate and the secrets exist.
   every pull request, where that environment's rules must not apply -- `verify-signing-key` on the
   same environment is "red by design" on foreign branches. The job waits for the Windows Java test
   job because it reads `natives-cpu-windows-x86-64`: `overwrite` deletes the artifact for a moment,
-  and a download in that moment fails. `package` lists the job in `needs` with an `if` that
-  tolerates its skip (`!cancelled() && !contains(needs.*.result, 'failure') && ...`): skipped means
-  no switch, run and failed (wrong secrets, the environment refusing the branch) means `package`
-  is skipped too, because the signature was asked for.
+  and a download in that moment fails. **The job runs on every run and is never skipped**: without
+  the switch its steps are gated off (`env.SIGN_WINDOWS`) and it takes the unprotected environment
+  `unsigned` -- the environment name is an expression, `maven-central` only when signing, so that
+  environment's rules never apply to a pull request -- and `package` lists it in `needs` with the
+  default condition: run and failed (wrong secrets, the environment refusing the branch) means
+  `package` is skipped, because the signature was asked for. The first form had the job itself
+  `if:`-skipped without the switch and `package` tolerating the skip (`!cancelled() &&
+  !contains(needs.*.result, 'failure') && ...`), and that lost every job behind `package`: GitHub
+  applies a skip "to all jobs in the dependency chain from the point of failure or skip onwards",
+  past any `if:` in between, so run 38870889665 (attempt 2, the first run to reach `package` after
+  the job existed, a dispatch on main) ended "success" with `package` green and the smoke sets, the
+  report and both publish jobs skipped -- and so did every run since, until this was corrected on
+  2026-10-22 (the fix had sat on the unmerged branch `claude/jolly-brahmagupta-0ydzby` since
+  2026-10-16). A job that other jobs depend on must run or fail, never skip.
 - **How**: `.github/sign-windows-natives.sh` -- **jsign** (`net.jsign:jsign:7.6` from Maven Central,
   pinned by version and SHA-256; Java, runs on the Linux runner, no signtool) with
   `--storetype ESIGNER --keystore  --storepass "|" --alias
@@ -3165,8 +3233,9 @@ certificate and the secrets exist.
   is on -- so a signed run cannot let an unsigned DLL into a jar, and a run without the switch is
   not blocked. Unit tests in `test_nativedeps.py` (`SignatureTest`, a synthetic PE with and without
   the security directory). `check-natives.py` holds the job itself (`natives.check_signing`): one
-  `overwrite: true` upload per Windows row of `natives.csv`, and the job waits for every job that
-  uploads one of them.
+  `overwrite: true` upload per Windows row of `natives.csv`, the job waits for every job that
+  uploads one of them, and it carries no job-level `if:` (the chain rule above; a step-level gate
+  is what it uses).
 - **What the owner does, in order** (TODO.md item 11 and "Code-sign the Windows DLLs" carry the
   details and the prices): delete the Azure Trusted Signing account; SSL.com's IV validation; the IV
   code-signing certificate enrolled in eSigner; the TOTP secret and the credential id from the
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ee1ae2fa3..9a0fb2405 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,13 +4,15 @@ Thank you for your interest in contributing! This document explains how to build
 
 ## How to build and run
 
-Prerequisites: Java 11+, Maven 3.x, CMake 3.22+, and a C++17-capable compiler.
+Prerequisites: a JDK 21 to build (Temurin; the library itself runs on Java 8+), a JDK 22+ registered in
+`~/.m2/toolchains.xml` for the `llama-ffm` module, Maven 3.x, CMake 3.22+, and a C++17-capable compiler.
 
 ```bash
-mvn compile                          # generate JNI headers
-cmake -B build && cmake --build build --config Release
-mvn test
-mvn package
+mvn -f llama/pom.xml compile                        # Java classes (src/main/cpp/jllama.h is kept by hand)
+cd llama && cmake -B build && cmake --build build --config Release && cd ..
+                                                    # native library into llama/src/main/natives/
+mvn -f llama/pom.xml test                           # model-backed tests skip without their GGUF
+mvn package                                         # the whole reactor
 ```
 
 See [README.md](README.md) for installation, Android, GPU (CUDA/Metal/Vulkan), and runtime usage. See [CLAUDE.md](CLAUDE.md) for the full maintainer guide (upstream-llama.cpp upgrade procedure, native helper architecture, per-version breaking-change table).
@@ -33,14 +35,14 @@ When reporting a bug, include: OS and architecture, `java -version` output, the
 
 ## Coding standards
 
-Follow the conventions documented in [CLAUDE.md](CLAUDE.md): native helper file split (`json_helpers.hpp` vs `jni_helpers.hpp`), include-order rule for upstream server headers, Javadoc HTML-entity table, and the upstream-llama.cpp upgrade procedure. Java targets 11+. C++ must be C++17-clean. Format C++ with `clang-format -i src/main/cpp/*.cpp src/main/cpp/*.hpp` before committing.
+Follow the conventions documented in [CLAUDE.md](CLAUDE.md): native helper file split (`json_helpers.hpp` vs `jni_helpers.hpp`), include-order rule for upstream server headers, Javadoc HTML-entity table, and the upstream-llama.cpp upgrade procedure. The core targets Java 8 bytecode (`llama-langchain4j` Java 17, `llama-ffm` Java 22). C++ must be C++17-clean. Format Java with `mvn spotless:apply` and C++ with clang-format 23.1.3 (`clang-format -i llama/src/main/cpp/*.cpp llama/src/main/cpp/*.hpp llama/src/test/cpp/*.cpp`) before committing.
 
 ## Test policy
 
 Every new feature or behaviour change MUST include automated tests; bug fixes SHOULD include a regression test.
 
-- Java tests: JUnit 4, under `src/test/java/net/ladenthin/llama/` and `src/test/java/examples/`. Require a model file (see CI configs for the HuggingFace download).
-- C++ unit tests: GoogleTest, under `src/test/cpp/` (`test_json_helpers.cpp`, `test_jni_helpers.cpp`, `test_server.cpp`, `test_utils.cpp`). No JVM or model file required.
+- Java tests: JUnit Jupiter, under `llama/src/test/java/net/ladenthin/llama/` and `llama/src/test/java/examples/`. Model-backed tests read their GGUF from `models/` (the list is `.github/models.csv`, which CI downloads) and skip themselves without it.
+- C++ unit tests: GoogleTest, under `llama/src/test/cpp/` (one file per helper header and per carried llama.cpp patch; CLAUDE.md lists them), built with `cmake -B build -DBUILD_TESTING=ON` and run with `ctest`. No JVM and no downloaded model required: the tests that need a model write a tiny random GGUF themselves.
 - Coverage is reported via JaCoCo and pushed to Coveralls and Codecov on every CI run; new code must not regress coverage on critical paths.
 
 ## Commit message convention
diff --git a/README.md b/README.md
index 7b8124987..92a28d7d7 100644
--- a/README.md
+++ b/README.md
@@ -11,15 +11,15 @@
 **Build:**  
 ![Java 8+](https://img.shields.io/badge/Java-8%2B-informational)  
 ![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20macOS%20%7C%20Windows%20%7C%20Android-lightgrey)  
-[![llama.cpp b11639](https://img.shields.io/badge/llama.cpp-%23b11639-informational)](https://github.com/ggml-org/llama.cpp/releases/tag/b11639)  
+[![llama.cpp b11815](https://img.shields.io/badge/llama.cpp-%23b11815-informational)](https://github.com/ggml-org/llama.cpp/releases/tag/b11815)  
 [![JPMS](https://img.shields.io/badge/JPMS-modular%20JAR-25A162)](https://openjdk.org/projects/jigsaw/)  
 ![JUnit](https://img.shields.io/badge/tested%20with-JUnit6-25A162)  
-[![JSpecify](https://img.shields.io/badge/JSpecify-1.0.0%20%40NullMarked-25A162)](https://jspecify.dev)  
+[![JSpecify](https://img.shields.io/badge/JSpecify-1.0.1%20%40NullMarked-25A162)](https://jspecify.dev)  
 [![NullAway](https://img.shields.io/badge/NullAway-strict%20JSpecify-25A162)](https://github.com/uber/NullAway)  
 [![Checker Framework](https://img.shields.io/badge/Checker%20Framework-Nullness-25A162)](https://checkerframework.org)  
 [![Error Prone](https://img.shields.io/badge/Error%20Prone-12%20patterns%20at%20ERROR-25A162)](https://errorprone.info)  
 [![Maven Enforcer](https://img.shields.io/badge/Maven%20Enforcer-strict-25A162)](https://maven.apache.org/enforcer/)  
-[![Lombok](https://img.shields.io/badge/Lombok-1.18.46-bc3f3c)](https://projectlombok.org/)  
+[![Lombok](https://img.shields.io/badge/Lombok-1.18.48-bc3f3c)](https://projectlombok.org/)  
 [![jqwik](https://img.shields.io/badge/tested%20with-jqwik-1f6feb)](https://jqwik.net)  
 [![ArchUnit](https://img.shields.io/badge/tested%20with-ArchUnit-c71a36)](https://www.archunit.org)  
 [![SpotBugs](https://img.shields.io/badge/analyzed%20with-SpotBugs-3b5998)](https://spotbugs.github.io)  
@@ -37,7 +37,7 @@
 [![Coverage Status](https://coveralls.io/repos/github/bernardladenthin/java-llama.cpp/badge.svg?branch=main)](https://coveralls.io/github/bernardladenthin/java-llama.cpp?branch=main)  
 [![codecov](https://codecov.io/gh/bernardladenthin/java-llama.cpp/graph/badge.svg)](https://codecov.io/gh/bernardladenthin/java-llama.cpp)  
 [![JaCoCo](https://img.shields.io/codecov/c/github/bernardladenthin/java-llama.cpp?label=JaCoCo&logo=java)](https://codecov.io/gh/bernardladenthin/java-llama.cpp)  
-[![PIT Mutation](https://img.shields.io/badge/PIT%20mutation-100%25%20(1%20class)-brightgreen)](https://github.com/bernardladenthin/java-llama.cpp/actions/workflows/publish.yml)  
+[![PIT Mutation](https://img.shields.io/badge/PIT%20mutation-100%25%20(value%2C%20exception%2C%20args%20%2B%205%20classes)-brightgreen)](https://github.com/bernardladenthin/java-llama.cpp/actions/workflows/publish.yml)  
 
 **Quality:**  
 [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=bernardladenthin_java-llama.cpp&metric=alert_status)](https://sonarcloud.io/dashboard?id=bernardladenthin_java-llama.cpp)  
@@ -111,12 +111,12 @@ Inference of Meta's LLaMA model (and others) in pure C/C++.
 - **Runtime LoRA adapter control** — list the loaded adapters and change their scales at runtime without reloading the model (`getLoraAdapters()` / `setLoraAdapters(Map)`), the typed counterpart of the upstream `GET`/`POST /lora-adapters` endpoints.
 - **Text-to-speech** (`TextToSpeech`) over llama.cpp's Qwen3-TTS pipeline (`mtmd_helper::gen_audio`), returning WAV audio.
 - **In-JVM GGUF quantization** (`LlamaQuantizer`) over llama.cpp's `llama_model_quantize` — convert a GGUF to another quantization scheme without shelling out to `llama-quantize`.
-- **Kolibri-1** (Aleph Alpha's German/English reasoning MoE, architecture `kolibri1`) before upstream llama.cpp supports it, through a carried patch that loads the GGUFs of both community converters. See [Kolibri-1](#kolibri-1-aleph-alpha).
+- **Kolibri-1** (Aleph Alpha's German/English reasoning MoE, architecture `kolibri1`), supported by llama.cpp itself since b11709; GGUFs converted with upstream's converter. See [Kolibri-1](#kolibri-1-aleph-alpha).
 - **Decision models** (`handleSystemOne`) — llama.cpp's TypeSafe-compatible `/v1/systemone` API: typed `choice` / `score` / `noul` questions about a state, answered with probabilities in one forward pass, no token generated (laya, julia-1, lev, openjev, kev and the later decision models). See [Decision models](#decision-models-v1systemone).
 - **Infilling** (fill-in-the-middle) for code models.
 - **Tokenize / detokenize** and **JSON-schema → grammar** conversion.
 - **Raw JSON endpoint handlers** mirroring the upstream llama.cpp HTTP server (`/completions`, `/v1/completions`, `/embeddings`, `/infill`, `/tokenize`, `/detokenize`).
-- **Two runnable HTTP server modes, one entry point.** The classes jar's `Main-Class` is `ServerLauncher`, which dispatches on the `--jllama-openai-compat` flag. Without it, `jbang --deps net.ladenthin:llama:6.0.0:cpu-linux-x86-64 net.ladenthin:llama:6.0.0 -m model.gguf --port 8080` runs the full upstream llama.cpp server (embedded **WebUI**, every llama-server flag forwarded) hosted inside `libjllama` over JNI — no separate `llama-server.exe`. With it, the same line plus `--jllama-openai-compat --model model.gguf --port 8080` runs the Java-transport, zero-extra-dependency **OpenAI-compatible** server (`OpenAiCompatServer`, streaming SSE) instead. Both are also runnable directly by class name via `java -cp … net.ladenthin.llama.server.{NativeServer,OpenAiCompatServer}`; see [Running the server from Maven Central](#running-the-server-from-maven-central).
+- **Two runnable HTTP server modes, one entry point.** The classes jar's `Main-Class` is `ServerLauncher`, which dispatches on the `--jllama-openai-compat` flag. Without it, `jbang --deps net.ladenthin:llama:6.0.0:cpu-linux-x86-64 net.ladenthin:llama:6.0.0 -m model.gguf --port 9931` runs the full upstream llama.cpp server (embedded **WebUI**, every llama-server flag forwarded) hosted inside `libjllama` over JNI — no separate `llama-server.exe`. With it, the same line plus `--jllama-openai-compat --model model.gguf --port 9931` runs the Java-transport, zero-extra-dependency **OpenAI-compatible** server (`OpenAiCompatServer`, streaming SSE) instead. Both are also runnable directly by class name via `java -cp … net.ladenthin.llama.server.{NativeServer,OpenAiCompatServer}`; see [Running the server from Maven Central](#running-the-server-from-maven-central).
 - **Model metadata** access (`getModelMeta()`) and **server management** (metrics, slot save/restore, runtime thread reconfiguration).
 - **Conversation checkpoints** — `Session.checkpoint(...)` / `rewind(...)` / `fork(...)` branch and roll back a chat (KV-cache slot save/restore + transcript snapshot) without re-prefilling.
 - **GGUF metadata inspection** without loading the model (`GgufInspector` — pure Java, reads header + key/value table only, big-endian aware).
@@ -124,7 +124,7 @@ Inference of Meta's LLaMA model (and others) in pure C/C++.
 - **Local agent** ([`llama-atmosphere-agent`](llama-atmosphere-agent/), JDK 21+) — a fully offline agent on top of this library that reads and edits files and, if allowed, runs commands. One agent session, three ways to use it: a **terminal** (full console or line-oriented `--plain`, fine over SSH/PuTTY), a **browser** (`--web`, token-protected, loopback by default — reach it from elsewhere through an SSH tunnel), and **IDEs** over the [Agent Client Protocol](https://agentclientprotocol.com) (`--acp`: JetBrains IDEs and Zed natively, VS Code through an ACP extension). See [Local agent](#local-agent-terminal-browser-ide-via-acp).
 - **Multi-model router mode** (`--models-dir` + per-request model selection, managed via the typed `RouterClient`) and **attach mode** (`NativeServer(LlamaModel, ...)` serves an already-loaded model over the full upstream HTTP frontend — one copy of the weights).
 - **HTTPS built in** — model downloads from `https://` URLs (`--model-url`, `-hf`) and a TLS-capable embedded server (`--ssl-key-file`/`--ssl-cert-file`) on every desktop platform, with BoringSSL linked statically: no OpenSSL to install, and no dependency on one (Android and s390x ship without SSL).
-- Pre-built native binaries for Linux (x86-64, aarch64, s390x), macOS (arm64, Metal included), Windows (x86-64, x86, arm64) and Android (arm64, x86-64), plus GPU backends (CUDA, Vulkan, OpenCL, ROCm/HIP, SYCL, OpenVINO) — one natives jar each, all loadable side by side with automatic CPU fallback; see [Choosing the natives jars](#choosing-the-natives-jars). Android additionally ships as the [`llama-android` AAR](#importing-in-android) with the optional `llama-kotlin` coroutines façade.
+- Pre-built native binaries for Linux (x86-64, aarch64, s390x), macOS (arm64, Metal included), Windows (x86-64, arm64) and Android (arm64, x86-64), plus GPU backends (CUDA, Vulkan, OpenCL, ROCm/HIP, SYCL, OpenVINO) — one natives jar each, loadable side by side and skipped when their vendor runtime is missing, so the model runs on the CPU modules; OpenVINO is opt-in (`-Dnet.ladenthin.llama.backend=openvino`). See [Choosing the natives jars](#choosing-the-natives-jars). Android additionally ships as the [`llama-android` AAR](#importing-in-android) with the optional `llama-kotlin` coroutines façade.
 
 ## Quick Start
 
@@ -146,7 +146,10 @@ In Gradle it is just `implementation("net.ladenthin:llama-platform:6.0.0")`.)
 > [!NOTE]
 > This layout starts with **6.0.0**. Up to 5.1.0, `net.ladenthin:llama` was one jar that
 > carried the CPU natives of every platform, and each GPU classifier was a complete
-> replacement for it.
+> replacement for it. Upgrading from 5.1? Everything a 5.1 consumer has to change -- the
+> dependency, the logging binding, the default port 9931 of both servers, renamed builder methods,
+> slot file names, requests refused instead of dropped -- is listed in one place:
+> [CHANGELOG.md, "Upgrading from 5.1"](CHANGELOG.md#upgrading-from-51).
 
 There are multiple [examples](llama/src/test/java/examples).
 
@@ -167,7 +170,7 @@ one more `//DEPS` line (see [Choosing the natives jars](#choosing-the-natives-ja
 
 ### Snapshot builds
 
-Every push to `main` publishes a snapshot to the [Sonatype Central snapshot repository](https://central.sonatype.com/repository/maven-snapshots/net/ladenthin/llama/).
+A run of the Publish workflow on `main` started by hand with `publish_to_central` enabled publishes a snapshot to the [Sonatype Central snapshot repository](https://central.sonatype.com/repository/maven-snapshots/net/ladenthin/llama/); a push to `main` alone builds and tests but does not deploy.
 
 To use the latest snapshot, add the repository and dependency to your `pom.xml`:
 
@@ -207,13 +210,16 @@ If any of these match your platform, you can include the Maven dependency and ge
 `net.ladenthin:llama` is the Java classes only. The native code ships as separate jars of the
 same artifact, selected by a Maven `` of the form `--`, of two kinds:
 
-- **One library jar per platform** (`cpu--`, `metal-macos-aarch64`): `libjllama` with
-  llama.cpp, ggml's shared libraries and the CPU backend modules (one per instruction-set level,
-  the best one for the running CPU is loaded at start). Exactly one is loaded, the one of the
-  running platform; `llama-platform` collects them for every desktop platform.
+- **One library jar per platform** (`cpu--`, `metal-macos-aarch64`): `libjllama` -- on
+  Linux x86-64 / aarch64, Windows and Android next to llama.cpp's and ggml's shared libraries
+  (`libllama`, `libmtmd`, `libggml*`) and the CPU backend modules (one per instruction-set level, of
+  which the best for the running CPU is loaded at start; a single one on Windows arm64), on macOS
+  and s390x one static library with llama.cpp and ggml linked in. Exactly one is loaded, the one of
+  the running platform; `llama-platform` collects them for every desktop platform.
 - **GPU module jars** (`cuda13-…`, `rocm-…`, `sycl-…`, `vulkan-…`, `opencl-…`, `openvino-…`): each
   holds only ggml's backend module for that GPU (`libggml-cuda.so`, `ggml-vulkan.dll`, …). They are
-  **additive**: the loader puts every GPU module jar of the platform next to the library, ggml loads
+  **additive**: the loader puts every GPU module jar of the platform next to the library (the opt-in
+  OpenVINO jar only when `net.ladenthin.llama.backend` names it, see below), ggml loads
   each module whose vendor runtime is installed and registers its devices, and a module whose runtime
   is missing is skipped with a log line — the model then runs on the CPU, so a GPU jar on a machine
   without that GPU just works. Several GPU jars together are fine (llama.cpp lists every device they
@@ -268,8 +274,8 @@ platforms you target, e.g. `cpu-linux-x86-64`.
 | `opencl-android-aarch64` | OpenCL (Adreno) | Android aarch64 with Adreno GPU | A device OpenCL ICD (`libOpenCL.so`); see also the `llama-android-opencl` AAR. |
 | `rocm-linux-x86-64` | ROCm / HIP | Linux x86-64 with AMD GPU | An AMD ROCm **10** runtime (`libamdhip64.so`, `librocblas.so`, `libhipblas.so`) — built against ROCm 10.0 (TheRock), like upstream llama.cpp; every GPU TheRock builds for Linux, Instinct included (gfx900/gfx906/gfx90c/gfx1153 best effort — built, but not release-ready in ROCm 10). |
 | `rocm-windows-x86-64` | ROCm / HIP | Windows x86-64 with AMD GPU | The AMD ROCm **10** runtime DLLs (`amdhip64.dll`, `rocblas.dll`, `hipblas.dll`) on `PATH`; every Radeon target TheRock builds for Windows, gfx900 through RDNA4 (gfx900/gfx906/gfx90c/gfx1153 best effort). |
-| `sycl-linux-x86-64` | SYCL (Intel oneAPI, fp16) | Linux x86-64 with Intel GPU (Arc / iGPU) | An Intel oneAPI / Level-Zero runtime. fp16 accumulation, as upstream's SYCL release builds. |
-| `sycl-windows-x86-64` | SYCL (Intel oneAPI, fp16) | Windows x86-64 with Intel GPU (Arc / iGPU) | The Intel oneAPI / Level-Zero runtime DLLs on `PATH`. |
+| `sycl-linux-x86-64` | SYCL (Intel oneAPI, fp16) | Linux x86-64 with Intel GPU (Arc / iGPU) | The Intel oneAPI 2026 runtime (`setvars.sh`: SYCL, MKL, oneDNN, TBB) and the Level Zero loader (`libze_loader.so.1`, part of Intel's GPU driver stack). fp16 accumulation, as upstream's SYCL release builds. |
+| `sycl-windows-x86-64` | SYCL (Intel oneAPI, fp16) | Windows x86-64 with Intel GPU (Arc / iGPU) | The Intel oneAPI 2026 runtime DLLs on `PATH` (`sycl9.dll`, `mkl_sycl_blas.6.dll`, `dnnl.dll`, `tbb12.dll`) and `ze_loader.dll` from Intel's graphics driver. A 2025.x runtime (`sycl8.dll`) no longer loads the module. |
 | `openvino-linux-x86-64` | OpenVINO | Linux x86-64 with an Intel GPU or NPU | **Opt-in** (see the note below): the jar does nothing until `-Dnet.ladenthin.llama.backend=openvino` names it. An Intel OpenVINO runtime **and an OpenCL ICD loader** (`libOpenCL.so.1`, package `ocl-icd-libopencl1` -- the OpenVINO archive does not ship it; without it the module silently does not open). |
 | `openvino-windows-x86-64` | OpenVINO | Windows x86-64 with an Intel GPU or NPU | **Opt-in** (see the note below): the jar does nothing until `-Dnet.ladenthin.llama.backend=openvino` names it. The Intel OpenVINO runtime DLLs on `PATH` (the OS supplies `OpenCL.dll`). |
 
@@ -281,7 +287,8 @@ platforms you target, e.g. `cpu-linux-x86-64`.
 > whose runtime is installed but finds no device (e.g. the CUDA toolkit without an NVIDIA GPU)
 > simply registers none -- **with two measured exceptions, both described below.** `-Dnet.ladenthin.llama.backend=,` restricts the modules put in
 > place to the named ones (`cpu` for none) and fails loud when a named jar is not on the
-> classpath; unset, every GPU module jar on the classpath is used.
+> classpath; unset, every GPU module jar on the classpath is used except the opt-in `openvino` one
+> (see below).
 
 > [!IMPORTANT]
 > **Two GPU jars need more than their runtime, and both were found by measurement rather than by
@@ -343,10 +350,10 @@ module jars, as `--deps`:
 
 ```bash
 # CPU only (Linux x86-64; cpu-windows-x86-64, metal-macos-aarch64, ... for the others)
-jbang --deps net.ladenthin:llama:6.0.0:cpu-linux-x86-64 net.ladenthin:llama:6.0.0 -m model.gguf --port 8080
+jbang --deps net.ladenthin:llama:6.0.0:cpu-linux-x86-64 net.ladenthin:llama:6.0.0 -m model.gguf --port 9931
 # with a GPU backend -- several may be named, ggml uses the ones whose runtime is installed
 jbang --deps net.ladenthin:llama:6.0.0:cpu-linux-x86-64,net.ladenthin:llama:6.0.0:vulkan-linux-x86-64 \
-    net.ladenthin:llama:6.0.0 -m model.gguf --port 8080 -ngl 99
+    net.ladenthin:llama:6.0.0 -m model.gguf --port 9931 -ngl 99
 ```
 
 Every llama-server flag works, the WebUI included; add `--jllama-openai-compat` to run the Java
@@ -355,7 +362,7 @@ Every llama-server flag works, the WebUI included; add `--jllama-openai-compat`
 profile per GPU module jar, named after its classifier:
 
 ```bash
-mvn -f examples/server/pom.xml exec:java -Dexec.args="-m model.gguf --port 8080"                      # CPU
+mvn -f examples/server/pom.xml exec:java -Dexec.args="-m model.gguf --port 9931"                      # CPU
 mvn -f examples/server/pom.xml -P cuda13-linux-x86-64 exec:java -Dexec.args="-m model.gguf -ngl 99"   # + CUDA
 # a local jar-with-dependencies of exactly that combination, for a machine without Maven
 mvn -f examples/server/pom.xml -P assembly,cuda13-linux-x86-64 package
@@ -363,9 +370,11 @@ java -jar examples/server/target/llama-server-6.0.0-jar-with-dependencies.jar -m
 ```
 
 The GitHub Release of a version attaches the Maven artifacts themselves — the classes jar, every
-natives jar, the poms, sources and javadoc, each with the detached GPG `.asc` that signed it for
-Central — and nothing else; a jar of every backend of every platform would be the opposite of
-modular natives.
+natives jar, the poms, sources and javadoc, the two Android AARs and the agent's jar (not
+`llama-ffm`'s, which is on Central only), each with the detached GPG `.asc` that signed it for
+Central — plus the signed llama.cpp source archive the natives were built from
+(`llama.cpp-b-jllama--src.tar.gz`), and nothing else; a jar of every backend of every
+platform would be the opposite of modular natives.
 
 ### Setup required
 
@@ -377,19 +386,19 @@ This consists of two steps: 1) Compiling the libraries and 2) putting them in th
 ##### Library Compilation
 
 First, have a look at [llama.cpp](https://github.com/ggerganov/llama.cpp/blob/master/docs/build.md) to know which build arguments to use (e.g. for CUDA support).
-Any build option of llama.cpp works equivalently for this project.
+Most build options of llama.cpp work the same way for this project; the ones it pins are overridden whatever you pass: `GGML_NATIVE` is always OFF, the x86 instruction-set flags are fixed at the Haswell level (AVX2, no AVX-512) in the default static build, `GGML_OPENMP` is OFF on Linux and Android, and `GGML_RPC` is ON with `GGML_RPC_RDMA` OFF.
 You then have to run the following commands in the `llama/` module directory (the native core lives
 there; the repository root is just the Maven reactor aggregator):
 
 ```shell
 cd llama       # the native core module
 mvn compile    # don't forget this line
-cmake -B build # add any other arguments for your backend, e.g. -DGGML_CUDA=ON
+cmake -B build # the library; on Linux and Windows add -DJLLAMA_BACKEND_DL=ON for the modular layout their jars ship
 cmake --build build --config Release
 ```
 
 > [!TIP]
-> Use `-DLLAMA_CURL=ON` to download models via Java code using `ModelParameters#setModelUrl(String)`.
+> Downloading a model from Java (`ModelParameters#setModelUrl(String)`, `setHfRepo(String)`) needs no build option: the build links BoringSSL statically and fetches `https://` URLs out of the box (not on Android, s390x or a Linux SYCL build). `-DLLAMA_CURL` is deprecated upstream and ignored.
 
 The library is put in a directory matching your platform and backend, which appears in the cmake output. For example:
 
@@ -400,13 +409,25 @@ The library is put in a directory matching your platform and backend, which appe
 `mvn test` puts that directory on the test classpath; `mvn -P natives package` turns it into a
 natives jar (in CI, together with every other platform's).
 
+A GPU backend is not an argument of this build: it is a ggml module next to a modular library. Build
+the library above with `-DJLLAMA_BACKEND_DL=ON` (Linux and Windows, x86-64 and aarch64; on Windows
+x86-64 with plain clang, `-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++`), then the module
+in a build directory of its own, which writes into the same natives tree: e.g.
+`cmake -B build-cuda -DJLLAMA_MODULE_ONLY=ON -DGGML_CUDA=ON` and
+`cmake --build build-cuda --config Release` (likewise `GGML_VULKAN`, `GGML_HIP`, `GGML_SYCL`,
+`GGML_OPENCL`, `GGML_OPENVINO`). A plain `cmake -B build -DGGML_CUDA=ON` writes its library into the
+`cuda13/` directory, from which the classpath loader takes no library; such a build loads only
+through `-Dnet.ladenthin.llama.lib.path=`.
+
 #### Library Location
 
 The loader looks for a **natives directory** -- the `jllama` library (`jllama.dll`, `libjllama.so`,
 `libjllama.dylib`) together with the files next to it: on the modular platforms ggml's shared
 libraries and the CPU backend modules, any GPU backend modules, and the three list files
 `jllama-build.txt`, `jllama-files.txt` and (on Windows) `jllama-extras.txt`. It searches in this
-order and stops at the first directory that holds the library:
+order and stops at the first directory from which the library loads (a directory whose library
+fails to load is reported on stderr and the next place is tried; on Android,
+`System.loadLibrary("jllama")`, which finds the AAR's `jni/`, is tried between the first two):
 
 - **net.ladenthin.llama.lib.path**: one directory you name, `-Dnet.ladenthin.llama.lib.path=/path/to/directory`.
   This is the "natives outside Maven" path, see the next section.
@@ -418,8 +439,9 @@ order and stops at the first directory that holds the library:
   [Choosing the natives jars](#choosing-the-natives-jars).
 
 The first two load **the same way as the third, minus the extraction**: the files
-`jllama-extras.txt` names are loaded first (on Windows `ggml-base.dll` and `ggml.dll`, which
-`jllama.dll` imports and which the system loader does not look for next to the DLL), then the
+`jllama-extras.txt` names are loaded first (on Windows `ggml-base.dll`, `ggml.dll`, `llama.dll` and
+`mtmd.dll`, which `jllama.dll` imports and which the system loader does not look for next to the
+DLL), then the
 library; ggml then loads every backend module that lies in the directory. The start-up line
 `[jllama] native backend '' loaded from  ...` says which branch loaded what.
 
@@ -456,8 +478,9 @@ java -cp 'llama-6.0.0.jar:lib/*' -Dnet.ladenthin.llama.lib.path=natives/net/lade
 
 To add a GPU backend, unpack its module jar the same way and copy the files of its directory
 (`net/ladenthin/llama/Linux/x86_64/cuda13/` for example) **flat** into the library's directory: the
-module library next to the CPU set; if the module ships a `jllama-extras.txt` (a vendor loader it
-needs preloaded), append its lines to the library's; its own `jllama-build.txt` and `jllama-sbom.json`
+module library next to the CPU set; its `jllama-files.txt` and, if it ships one, its
+`jllama-extras.txt` (a vendor loader it needs preloaded) are appended to the library's files of the
+same name rather than copied over them; its own `jllama-build.txt` and `jllama-sbom.json`
 are not needed (the module and the library must come from the same `net.ladenthin:llama` version,
 which the loader checks when the jars are on the classpath and you check by taking both from the same
 version here). What the directory tells you: `jllama-build.txt` names the llama.cpp tag and commit,
@@ -468,15 +491,15 @@ stderr at start. `net.ladenthin.llama.backend` has no effect on a directory (ggm
 that lies in it); the loader says so when it is set. Nothing is extracted and nothing is written:
 the directory may be read-only.
 
-**The directory is also a C library.** On the modular platforms (Linux, Windows, Android) it holds
+**The directory is also a C library.** On the modular platforms (Linux x86-64 / aarch64, Windows, Android) it holds
 upstream's own `libllama` / `libmtmd` / `libggml*` as shared libraries built from this project's
 patched source -- the same files and symbols as upstream's release zips, usable with upstream's
 `llama.h` / `mtmd.h` / `ggml.h` -- and on every platform `libjllama` exports this project's own
 **C API** (`jllama_api.h`, shipped in the directory, with the import library `jllama.lib` on
 Windows): everything the Java API offers -- model loading, chat and raw completions, streaming,
 embeddings, rerank, tokenisation, metrics and slots, LoRA, the embedded HTTP server, the RPC
-server, text to speech, quantisation, fine-tuning, logging -- callable from C, Rust, Python or
-Java 22+ without a JVM; the Java entry points are shells over the same functions. See
+server, text to speech, quantisation, fine-tuning, logging -- callable from C, Rust or Python without a JVM, and from Java 22+ without JNI; the Java entry
+points are shells over the same functions. See
 [docs/c-api.md](docs/c-api.md).
 
 **Java 22+ without JNI: `net.ladenthin:llama-ffm`.** The same C API through the Foreign Function
@@ -498,8 +521,8 @@ Every `net.ladenthin.llama.*` system property recognised by the library, deep-sc
 | `net.ladenthin.llama.lib.path` | unset (falls back to `java.library.path`, then the natives jars) | runtime | `LlamaLoader` | A natives directory (see [Natives outside Maven](#natives-outside-maven-the-natives-directory)): the `jllama` library with the files next to it, loaded with the same logic as from the natives jars but without extraction. Checked first. Set with `-Dnet.ladenthin.llama.lib.path=/path/to/dir`. |
 | `net.ladenthin.llama.tmpdir` | unset (falls back to `java.io.tmpdir`) | runtime | `LlamaLoader` | Directory the natives are extracted into, one subdirectory per backend and build (`jllama-backend--`). It is kept across runs: the next start of the same build compares each file with the jar and copies nothing; a directory of a build that is no longer on the classpath is removed by a later start once it is 10 minutes old. Point it elsewhere when the default temp directory is on a slow or policy-restricted volume. |
 | `net.ladenthin.llama.osinfo.architecture` | unset (uses `os.arch`) | runtime | `OSInfo` | Override for the architecture string used to locate the bundled library inside the JAR. Useful when `os.arch` reports an unexpected value (e.g. inside dockcross / chrooted environments). |
-| `net.ladenthin.llama.backend` | unset (every GPU module jar on the classpath is put next to the library) | runtime | `LlamaLoader` | A comma-separated filter over the GPU module jars (e.g. `vulkan`, `cuda13,vulkan`): only the named modules are put in place, `cpu` names none; a named module whose jar is not on the classpath, or an unknown name, fails the load. The library jar is never a choice — it is the one of the running platform. See [Choosing the natives jars](#choosing-the-natives-jars). |
-| `net.ladenthin.llama.test.ngl` | `43` for the general suite; `0` for `ToolCallingIntegrationTest` | test | Model-backed integration tests | Number of GPU layers used during testing. Pin to `0` on CPU-only hosts: `mvn test -Dnet.ladenthin.llama.test.ngl=0`. The tool test also selects device `none` at zero layers so Metal/CUDA is not initialized. |
+| `net.ladenthin.llama.backend` | unset (every GPU module jar on the classpath is put next to the library, except the opt-in `openvino` one) | runtime | `LlamaLoader` | A comma-separated filter over the GPU module jars (e.g. `vulkan`, `cuda13,vulkan`): only the named modules are put in place, `cpu` (or `metal`) names none, and an opt-in module (`openvino`) is used only when named; a named module whose jar is not on the classpath, or an unknown name, fails the load. The library jar is never a choice — it is the one of the running platform. See [Choosing the natives jars](#choosing-the-natives-jars). |
+| `net.ladenthin.llama.test.ngl` | `43` for the general suite; `0` for `ToolCallingIntegrationTest`, `TtsIntegrationTest` and `ReactorIntegrationTest` | test | Model-backed integration tests | Number of GPU layers used during testing. Pin to `0` on CPU-only hosts: `mvn test -Dnet.ladenthin.llama.test.ngl=0`. The tool test also selects device `none` at zero layers so Metal/CUDA is not initialized. |
 | `net.ladenthin.llama.text.model` | `models/codellama-7b.Q2_K.gguf` (tests self-skip if missing) | test | `LlamaModelTest` and most model-backed tests (`TestConstants.MODEL_PATH`) | Path to the main text-generation GGUF. Most tests only need an instruct-capable model; a few assertions are tuned to the CI model. |
 | `net.ladenthin.llama.draft.model` | `models/AMD-Llama-135m-code.Q2_K.gguf` (tests self-skip if missing) | test | Speculative-decoding tests and the tests that need a small, fast model (`TestConstants.DRAFT_MODEL_PATH`) | Path to the draft GGUF. |
 | `net.ladenthin.llama.reasoning.model` | `models/Qwen3-0.6B-Q4_K_M.gguf` (tests self-skip if missing) | test | Reasoning-budget tests (`TestConstants.REASONING_MODEL_PATH`) | Path to a thinking model (one that emits a thinking block). |
@@ -516,6 +539,11 @@ Every `net.ladenthin.llama.*` system property recognised by the library, deep-sc
 | `net.ladenthin.llama.tts.model` | `models/Qwen3-TTS-12Hz-1.7B-Base-Q4_K_M.gguf` (test self-skips if missing) | test | `TtsIntegrationTest` | Path to the Qwen3-TTS backbone (text) GGUF. Any Qwen3-TTS-family model works; CI default is `Qwen3-TTS-12Hz-1.7B-Base-Q4_K_M.gguf`. |
 | `net.ladenthin.llama.tts.mmproj` | `models/mmproj-Qwen3-TTS-12Hz-1.7B-Base-Q8_0.gguf` (test self-skips if missing) | test | `TtsIntegrationTest` | Path to the matching Qwen3-TTS mmproj GGUF (speaker encoder + code predictor + code2wav decoder); CI default is `mmproj-Qwen3-TTS-12Hz-1.7B-Base-Q8_0.gguf`. |
 | `net.ladenthin.llama.train.model` | `models/stories260K.gguf` (test self-skips if missing) | test | `LlamaTrainerIntegrationTest` | Path to the model the fine-tuning smoke trains. Must be **F32**: `llama_set_param` skips every other tensor type, so a quantized model would train nothing. |
+| `net.ladenthin.llama.model.path` | the `reasoning.model` path (`models/Qwen3-0.6B-Q4_K_M.gguf`; tests self-skip if missing) | test | `IdleSleepWakeIntegrationTest`, `SessionForkRewindIntegrationTest` | Path to the model these two tests load; unset, they use the reasoning model. |
+| `net.ladenthin.llama.server.model` | the `text.model` path (`models/codellama-7b.Q2_K.gguf`) | test | `examples.OpenAiServerExample` (a `@Disabled` demo, run by hand) | GGUF the demo's OpenAI-compatible endpoint serves. |
+| `net.ladenthin.llama.server.port` | `9931` | test | `examples.OpenAiServerExample` | Port of the demo's endpoint. |
+
+The other modules read their own test properties (`net.ladenthin.llama.ffm.model`, `net.ladenthin.llama.langchain4j.*`), documented in [`llama-ffm/README.md`](llama-ffm/README.md) and [`llama-langchain4j/README.md`](llama-langchain4j/README.md).
 
 The test-model defaults are exactly CI's model set (`.github/models.csv`, URLs included), so a model downloaded into `models/` is found without any property; the properties only point a test at another file. `MultimodalIntegrationTest` self-skips when any of the three `vision.*` paths is missing, so a partial setup (just the vision model + the committed image, no mmproj) lets the test class load without erroring. `AudioInputIntegrationTest` self-skips the same way over the three `audio.*` properties. `TtsIntegrationTest` likewise self-skips unless both `tts.model` and `tts.mmproj` paths exist, and `LlamaTrainerIntegrationTest` unless `train.model` (default `models/stories260K.gguf`, an **F32** model) does.
 
@@ -579,8 +607,7 @@ try (LlamaModel model = new LlamaModel(modelParams)) {
     }
     // Calculate a whole response before returning it.
     String response = model.complete(inferParams);
-    // Returns the hidden representation of the context + prompt.
-    float[] embedding = model.embed("Embed this");
+    // Embeddings need a model loaded with enableEmbedding(), see "Embeddings & Reranking" below.
 }
 ```
 
@@ -653,7 +680,8 @@ every other llama-server flag.
 
 `setMmprojDevice(...)` and `setMmprojOffload(...)` write the **same** upstream field
 (`common_params::mmproj_use_gpu`), so where they disagree the outcome would depend on argv order —
-and the rendered argv comes from a `HashMap`, whose order is unspecified. The builder therefore
+and the rendered argv keeps each option at the position where it was first set (a `LinkedHashMap`),
+so a later call does not necessarily come last. The builder therefore
 resolves the two genuinely ambiguous combinations by dropping the earlier call, and leaves the rest
 alone:
 
@@ -662,7 +690,7 @@ alone:
 | named device + `setMmprojOffload(true)` | `(use_gpu=true, device)` in either order | both kept — no clash |
 | named device + `setMmprojOffload(false)` | order-dependent | **last call wins** |
 | `"none"` + `setMmprojOffload(true)` | order-dependent | **last call wins** |
-| `"none"` + `setMmprojOffload(false)` | `(use_gpu=false)` in either order | both kept — no clash |
+| `"none"` + `setMmprojOffload(false)` | `(use_gpu=false)` in either order | both kept when `"none"` is set last; `setMmprojOffload(false)` after it drops the `"none"` (same end state) |
 
 So a multi-GPU projector pin survives an explicit `setMmprojOffload(true)`; only a call that would
 actually contradict the other is dropped. If you need a device after disabling offload, call
@@ -671,16 +699,17 @@ actually contradict the other is dropped. If you need a device after disabling o
 **Video input — decode settings only, so far.** `mtmd` has carried a video path since llama.cpp
 **b9562** (#24269); **b10647** (#24318) added the `--video-*` CLI flags and the
 `mtmd_helper_init_opt` plumbing that surfaces them. It is compiled into the shipped desktop library
-(`MTMD_VIDEO` is on by default, gated on `LLAMA_SUBPROCESS`, which upstream force-disables on
-Android and iOS). Its decode settings are exposed
+(`MTMD_VIDEO` is on by default and forced off when `LLAMA_SUBPROCESS` is off, which is upstream's
+default on Android and iOS). Its decode settings are exposed
 as `setVideoFps(float)`, `setVideoTimestampInterval(long)` and `setVideoFfmpegDir(String)`. The last
 one matters most in a JVM: upstream shells out to `ffmpeg`/`ffprobe` and resolves them from `PATH`,
 which an application server, an Android app or a JAR-only container frequently does not have them on —
 naming the directory is then the only way for video to work at all.
 
 What is **not** here yet is the content part: upstream's wire type for a video is
-`{"type":"input_video","input_video":{"data":""}}` (raw base64, not a `data:` URI, unlike
-`image_url`), gated server-side on `mtmd_helper_support_video`. `ContentPart` has no `videoFile(...)`
+`{"type":"input_video","input_video":{"data":"..."}}` (OpenAI's `video_url` is accepted as an alias; as
+for `image_url`, the value may be raw base64, a `data:video/...;base64,` URI or an HTTP(S) URL),
+gated server-side on `mtmd_helper_support_video`. `ContentPart` has no `videoFile(...)`
 factory emitting that shape, so these knobs currently configure a path this API cannot yet feed
 directly. Tracked in `TODO.md`.
 
@@ -711,8 +740,9 @@ try (LlamaModel model = new LlamaModel(modelParams)) {
 ### Tool Calling
 
 Use a tool-aware instruct model and enable Jinja when loading it. A typed request can either return
-the model's tool calls through `chat`, or execute registered handlers until the model produces a
-normal assistant response through `chatWithTools`:
+the model's tool calls through `chat`, or execute registered handlers through `chatWithTools` until the model produces a normal assistant
+response or `ChatRequest#withMaxToolRounds(int)` chat calls (default 8) have been made; the last
+response is then returned as it is, tool calls included:
 
 ```java
 ToolDefinition weather = new ToolDefinition(
@@ -744,7 +774,10 @@ unknown tool names are returned to the model as valid `{"error":"..."}` tool-res
 
 ### Infilling
 
-You can simply set `InferenceParameters#withInputPrefix(String)` and `InferenceParameters#withInputSuffix(String)`.
+Set both `InferenceParameters#withInputPrefix(String)` and `InferenceParameters#withInputSuffix(String)`;
+`complete`, `completeWithStats` and `generate` then format the prompt around the model's fill-in-the-middle tokens,
+exactly as `handleInfill` does (an optional `withPrompt` is appended to the prefix). A request with only one of
+the two fields is an `InvalidRequestException`, a model without FIM tokens a `LlamaException`.
 
 ### Embeddings & Reranking
 
@@ -765,12 +798,17 @@ try (LlamaModel model = new LlamaModel(modelParams)) {
 ### Kolibri-1 (Aleph Alpha)
 
 [Kolibri-1](https://huggingface.co/Aleph-Alpha/Kolibri-1) is Aleph Alpha's 78B-parameter Mixture-of-Experts
-reasoning model for German and English (about 3.5B parameters active per token, Apache-2.0). Upstream llama.cpp
-does not support its architecture yet ([ggml-org/llama.cpp#29922](https://github.com/ggml-org/llama.cpp/issues/29922)),
-so this library carries it as a patch (`llama/patches/0016-model-kolibri1.patch`) until upstream does. It loads the
-community GGUFs of both published converters -- e.g. [Hob-forge/Kolibri-1-GGUF](https://huggingface.co/Hob-forge/Kolibri-1-GGUF)
-and [Eliasfpv28/Kolibri-1-Q3_K_S-GGUF](https://huggingface.co/Eliasfpv28/Kolibri-1-Q3_K_S-GGUF) -- which the two
-community llama.cpp patches cannot each load from the other. The embedded chat template handles reasoning and
+reasoning model for German and English (about 3.5B parameters active per token, Apache-2.0). llama.cpp supports
+the architecture since b11709 ([ggml-org/llama.cpp#30673](https://github.com/ggml-org/llama.cpp/pull/30673)), and
+this library has carried it as a patch before that (`0016-model-kolibri1.patch`, dropped at the b11760 bump). Use a
+GGUF converted with upstream's `convert_hf_to_gguf.py` at b11709 or later: the GGUFs the two community ports
+published earlier were written before upstream's converter existed, and the dropped patch tolerated metadata
+upstream does not: [Hob-forge/Kolibri-1-GGUF](https://huggingface.co/Hob-forge/Kolibri-1-GGUF) uses a
+pre-tokenizer name upstream does not know (`kolibri1`) and a tied output, both refused at load (its other
+gating-function value is not checked there; on its own it would abort the process when the context is
+created); [Eliasfpv28/Kolibri-1-Q3_K_S-GGUF](https://huggingface.co/Eliasfpv28/Kolibri-1-Q3_K_S-GGUF) uses
+upstream's gating value and pre-tokenizer and loads only if it carries every key upstream requires (not
+verified here). The embedded chat template handles reasoning and
 Hermes-style tool calls; for a split GGUF, point the model path at the first part.
 
 ```java
@@ -785,8 +823,8 @@ try (LlamaModel model = new LlamaModel(params)) {
 ```
 
 The model is large (the Q4_K_M file is 47.5 GB) and runs from system RAM on the CPU, or partly offloaded to a
-GPU. The architecture is checked numerically against Aleph Alpha's reference on tiny random models in every C++
-test run; the real model was not run in this project's CI, and the GPU backends are untested for it.
+GPU. Upstream's implementation is checked numerically against Aleph Alpha's reference on tiny random models in
+every C++ test run of this project (`test_kolibri1.cpp`); the real model was not run in this project's CI.
 
 ### Decision models (`/v1/systemone`)
 
@@ -830,8 +868,11 @@ try (LlamaModel model = new LlamaModel(modelParams)) {
 ```
 
 Per the upstream contract, a scale update lists the adapters to keep active — any adapter missing
-from the map is set to scale `0` (disabled). The native side clears affected KV caches when the
-effective adapter set changes.
+from the map is set to scale `0` (disabled). A slot whose cache was computed under other scales clears
+it at its next request, so no answer mixes old and new adapters (upstream's `POST /lora-adapters`
+path does not; `patches/0021` applies upstream's rule for per-request adapters there too). The
+host-memory prompt cache (`--cache-ram`, on by default) does not record the adapters and can still load a
+state saved under other scales; disable it (`setCacheRamMib(0)`) where that matters.
 
 ### Text-to-Speech
 
@@ -877,10 +918,16 @@ For direct access to the upstream llama.cpp server API, the following methods ta
 a JSON response, matching the HTTP server's contract:
 
 `handleCompletions`, `handleCompletionsOai`, `handleChatCompletions`, `handleInfill`,
-`handleEmbeddings`, `handleTokenize`, `handleDetokenize`.
+`handleEmbeddings` (its second argument, `oaiCompat`, selects the OpenAI response format).
+
+`handleTokenize(String content, boolean addSpecial, boolean withPieces)` and `handleDetokenize(int... tokens)`
+take plain arguments and return the JSON response of the server's `/tokenize` and `/detokenize` routes.
 
 Server state is exposed via `getMetrics()`, `eraseSlot(int)`, `saveSlot(int, String)`,
-`restoreSlot(int, String)`, and `getModelMeta()`.
+`restoreSlot(int, String)`, and `getModelMeta()`. Saving and restoring a slot need a slot directory
+(`ModelParameters.setSlotSavePath`, `--slot-save-path`), and the `String` is a file name inside it, as
+the server's `/slots` route takes it -- a name with a directory part or `..` is an
+`InvalidRequestException`.
 
 ### Conversation checkpoints: rewind + fork (`Session`)
 
@@ -888,16 +935,18 @@ A `Session` can be snapshotted and branched — the KV-cache slot state and the
 together, so native state and history can never drift apart:
 
 ```java
+// The model is loaded with a slot directory (ModelParameters.setSlotSavePath("checkpoints")); a
+// checkpoint is a file name inside it, as for LlamaModel.saveSlot.
 try (Session session = new Session(model, 0, "You are terse.")) {
     session.send("My name is Alice.");
-    SessionCheckpoint cp = session.checkpoint("checkpoints/turn1.bin");
+    SessionCheckpoint cp = session.checkpoint("turn1.bin");
 
     session.send("Tell me a joke.");
     session.rewind(cp);                     // undo everything after the checkpoint
     session.send("Tell me a story instead."); // retry from the branch point
 
     // Branch into a second slot (model loaded with setParallel(2)+):
-    try (Session forked = session.fork(1, "checkpoints/branch.bin")) {
+    try (Session forked = session.fork(1, "branch.bin")) {
         forked.send("Answer as a pirate.");   // both sessions continue independently
     }
 }
@@ -945,8 +994,10 @@ speculative-decoding tallies (`getDraftTokensTotal()`, `getDraftAcceptedTotal()`
 `getDraftVerifyStepsTotal()`, `getDraftAcceptedPerPosition()`, plus the derived
 `getDraftAcceptanceRate()`), which upstream otherwise exposes only as Prometheus text.
 
-The embedded HTTP server exposes the same native JSON at authenticated `GET /metrics`, with the slot
-array alone at `GET /slots`. OpenAI responses preserve
+`OpenAiCompatServer` exposes the same JSON at `GET /metrics` (behind the API key when one is
+configured), with the slot array alone at `GET /slots`; `NativeServer` serves upstream's routes
+instead, where `GET /metrics` is Prometheus text and needs `--metrics`
+(`ModelParameters.enableMetricsEndpoint()` in attach mode). OpenAI responses preserve
 `usage.prompt_tokens_details.cached_tokens`; Responses API output uses
 `usage.input_tokens_details.cached_tokens`; Anthropic output uses `cache_read_input_tokens`.
 
@@ -962,9 +1013,9 @@ built-in WebUI" below). It serves:
 | Method & path | Backed by |
 |---|---|
 | `POST /v1/chat/completions` | `LlamaModel.streamChatCompletion` (streaming SSE) / `chatComplete` (blocking) |
-| `POST /v1/completions` | `LlamaModel.handleCompletionsOai` |
+| `POST /v1/completions` | `LlamaModel.streamCompletionsOai` (streaming SSE) / `handleCompletionsOai` (blocking) |
 | `POST /v1/embeddings` (requires `--embedding`) | `LlamaModel.handleEmbeddings` |
-| `POST /v1/rerank` (requires `--reranking`) | `LlamaModel.handleRerank` (reshaped to `results`/`data`) |
+| `POST /v1/rerank` (requires `--reranking`) | `LlamaModel.handleRerank` (reshaped to `results`/`data` with `usage`; a request with TEI's `texts` is answered as TEI's bare `[{index, score[, text]}]` array, `text` on `return_text`, as upstream) |
 | `POST /infill` | `LlamaModel.handleInfill` (fill-in-the-middle autocomplete) |
 | `GET /v1/models` | the configured model id |
 | `GET /metrics` | native server and per-slot token/cache counters (JSON) |
@@ -975,8 +1026,8 @@ Chat completions support **streaming via Server-Sent Events** and non-streaming,
 `messages`/`tools` verbatim. The streaming path carries `delta.tool_calls` and (with
 `stream_options.include_usage`) a trailing `usage` chunk, so **agent/tool-calling clients work** —
 this is the recommended surface for VS Code Copilot agent mode, Cline, Roo Code and Continue.
-`response_format` (`json_object` / `json_schema`) is forwarded for structured outputs. Completions,
-embeddings, rerank and infill are non-streaming.
+`response_format` (`json_object` / `json_schema`) is forwarded for structured outputs. `/v1/completions` streams with `"stream": true` (upstream's own `text_completion` chunks, `usage` on the
+last); embeddings, rerank and infill are non-streaming.
 
 Every route is also reachable **without the `/v1` prefix**, the server answers **CORS preflight**
 (`OPTIONS`) and stamps `Access-Control-Allow-Origin` (so browser/webview clients work), and
@@ -984,6 +1035,10 @@ Every route is also reachable **without the `/v1` prefix**, the server answers *
 (llama.vscode, Twinny, Tabby, Continue's `llama.cpp` provider). Note: GitHub Copilot's **inline**
 completions cannot be served by any local endpoint — only its chat/agent surfaces — so use one of
 those autocomplete plugins for ghost text.
+Continue's **Ollama** provider decides FIM support from a `{{ .Suffix }}` in the model's `template`
+(`/api/show`), which this server leaves empty — it does not know the model's prompt format, and an
+invented template would be wrong for most models. Continue then uses its own prompt templates for
+autocomplete; for the model's own FIM tokens point it at `POST /infill` (its `llama.cpp` provider).
 
 **Alternative protocol surfaces.** For clients that don't speak OpenAI Chat Completions, the same model
 is exposed through additional protocols (pure translation over the OpenAI core — no extra inference
@@ -991,20 +1046,50 @@ path), all supporting tools and streaming:
 
 | Surface | Routes | For |
 |---|---|---|
-| **Ollama-native** | `GET /api/version`, `GET /api/tags`, `POST /api/show`, `POST /api/chat` (NDJSON streaming), `POST /api/generate` (prompt completion / FIM) | Copilot's built-in **Ollama** provider; Ollama-hardcoded tools |
-| **Anthropic Messages** | `POST /v1/messages` (SSE event stream) | Claude-shaped clients (Claude Code); Copilot `messages` apiType |
-| **OpenAI Responses** | `POST /v1/responses` (SSE event stream) | Copilot `responses` apiType; Responses-API clients |
+| **Ollama-native** | `GET /api/version`, `GET /api/tags`, `POST /api/show`, `POST /api/chat` (NDJSON streaming), `POST /api/generate` (through the chat template with `system`, as Ollama does -- a request's own `template` and a token `context` are refused there, since the model's chat template renders the prompt and no answer hands out a context; the bare prompt with `raw: true`, where `format` constrains the answer and each image goes where its `[img-N]` tag stands, as in Ollama -- an image no tag places is refused; FIM with `suffix`, which takes `format` too but no images; a `format` other than `"json"` or a schema object is refused; without a prompt and without a suffix Ollama's "model loaded" answer; unlike Ollama, a request with only a suffix stays fill-in-the-middle at the start of the file, and `keep_alive: 0` does not unload and is answered "load") | Copilot's built-in **Ollama** provider; Ollama-hardcoded tools |
+| **Anthropic Messages** | `POST /v1/messages` (SSE event stream), `POST /v1/messages/count_tokens` (`{"input_tokens": N}`, the prompt `/v1/messages` would evaluate; text only, a request with an image is answered 501) | Claude-shaped clients (Claude Code); Copilot `messages` apiType |
+| **OpenAI Responses** | `POST /v1/responses` (SSE event stream), `POST /v1/responses/input_tokens` (upstream's token count, `{"input_tokens": N, "object": "response.input_tokens"}`; the same for chat at `POST /v1/chat/completions/input_tokens`) | Copilot `responses` apiType; Responses-API clients |
 
 `/api/show` advertises the model's capabilities (`tools`, `insert`, and `vision` when `--mmproj` is set)
 and context length, which Copilot's Ollama provider reads to enable agent mode. The llama.cpp-native
 `GET /props` reports `default_generation_settings.n_ctx` and a `modalities` block, which autocomplete
 clients such as llama.vscode read to size their context window.
 
+**Request checks on the translated routes.** The chat routes (OpenAI chat, Anthropic Messages, Ollama,
+OpenAI Responses) build the native request themselves rather than forwarding the body, so they check
+what llama-server checks and answer `400` (Anthropic's `invalid_request_error`, Ollama's `{"error":…}`)
+where it does; where llama-server would drop part of a request without a word, they refuse it instead:
+
+- A number field (`max_tokens`, `temperature`, `n`, `n_probs`, `seed`, the llama.cpp sampling
+  extensions, Ollama's `options`, …) holding anything but a JSON number is refused with llama-server's
+  message (`Field 'max_tokens': type must be number, but is string`); `null` is the default, a fraction in
+  an integer field is truncated, as there. Stricter than llama-server: a boolean is refused (llama-server
+  reads it as 0 or 1), and so is a mistyped `top_logprobs`, `id_slot` or reasoning budget, where
+  llama-server logs a warning and uses the default.
+- `tool_choice` naming one tool (OpenAI `{"type":"function","function":{"name":…}}`, Responses
+  `{"type":"function","name":…}`, Anthropic `{"type":"tool","name":…}`) forwards that tool alone with
+  `required`, so the model can call no other; a name no tool carries is refused. llama-server reads the
+  first two as `auto` and the Anthropic one as `required` over every tool. A `tool_choice` object of any
+  other type (`allowed_tools`, `custom`, `mcp`, a hosted tool) cannot be expressed in llama.cpp's
+  `auto`/`none`/`required` and is refused, where llama-server runs the request as `auto`; so is a value
+  that is neither a string nor an object (on Anthropic, one that is no object).
+- `logprobs: true` on a streamed chat request with tools is refused with llama-server's own message
+  (`logprobs is not supported with tools + stream`); blocking, or without tools, it is answered.
+- An Anthropic `base64` image without `media_type` is named by its bytes (PNG, JPEG, GIF, WebP) and as
+  `image/jpeg` otherwise; llama-server names every such image `image/jpeg`. The type is informational,
+  llama.cpp decodes the image by its content.
+- An Anthropic image whose source is neither `base64` nor `url` (the Files API's `file`, which a local
+  server cannot fetch) or lacks its `data` / `url` is refused (`image source type 'file' is not
+  supported; send base64 or url`), in a message and inside a `tool_result`; llama-server drops such an
+  image without a word, and the model answers as if none had been sent.
+
 Embed it in your app:
 
 ```java
-ModelParameters modelParams = new ModelParameters().setModel("models/model.gguf").setParallel(2);
-OpenAiServerConfig config = OpenAiServerConfig.builder().port(8080).modelId("local-model").build();
+// the alias is the name llama.cpp puts into every answer; set it to the id /v1/models advertises
+ModelParameters modelParams =
+        new ModelParameters().setModel("models/model.gguf").setParallel(2).setAlias("local-model");
+OpenAiServerConfig config = OpenAiServerConfig.builder().port(9931).modelId("local-model").build();
 try (LlamaModel model = new LlamaModel(modelParams);
      OpenAiCompatServer server = new OpenAiCompatServer(model, config).start()) {
     Thread.currentThread().join(); // serve until interrupted
@@ -1018,17 +1103,17 @@ or name the class explicitly via `-cp`:
 ```bash
 # from Maven Central (JBang resolves the classes jar, the natives jar named and the Java deps)
 jbang --deps net.ladenthin:llama:6.0.0:cpu-linux-x86-64 net.ladenthin:llama:6.0.0 --jllama-openai-compat \
-    --model models/Qwen3-0.6B-Q4_K_M.gguf --host 0.0.0.0 --port 8080 --n-gpu-layers 99
+    --model models/Qwen3-0.6B-Q4_K_M.gguf --host 0.0.0.0 --port 9931 --n-gpu-layers 99
 
 # or name the class explicitly, with the jars on the classpath
 java -cp 'target/llama-.jar:target/llama--cpu-linux-x86-64.jar:' \
-  net.ladenthin.llama.server.OpenAiCompatServer --model models/model.gguf --port 8080 --model-id local-model
+  net.ladenthin.llama.server.OpenAiCompatServer --model models/model.gguf --port 9931 --model-id local-model
 ```
 
 Run with `--help` for the full option list (`-m/--model`, `--host`, `-p/--port`, `-c/--ctx-size`,
 `-b/--batch-size`, `-ub/--ubatch-size`, `-ngl/--n-gpu-layers`, `-t/--threads`, `-tb/--threads-batch`,
 `-ctk/--cache-type-k`, `-ctv/--cache-type-v`, `--jinja`, `--chat-template-kwargs`, `--parallel`,
-`--model-id`, `--api-key`, `--mmproj`, `-mmdev/--mmproj-device`, `--embedding`, `--reranking`). The tuning flags mirror
+`--model-id`, `--api-key`, `--mmproj`, `-mmdev/--mmproj-device`, `--rpc`, `--embedding`, `--reranking`). The tuning flags mirror
 llama.cpp's server, so an invocation like
 `--jinja --chat-template-kwargs '{"reasoning_effort":"low"}' -ctk q8_0 -ctv q8_0 -b 4096 -ub 2048`
 works directly.
@@ -1036,7 +1121,7 @@ works directly.
 Verify with curl (streaming chat):
 
 ```bash
-curl -N http://127.0.0.1:8080/v1/chat/completions \
+curl -N http://127.0.0.1:9931/v1/chat/completions \
   -H 'Content-Type: application/json' \
   -d '{"model":"local-model","stream":true,"messages":[{"role":"user","content":"hi"}]}'
 ```
@@ -1057,7 +1142,7 @@ endpoint (the host/port go here, not in the form):
       {
         "id": "local-model",
         "name": "Local model",
-        "url": "http://127.0.0.1:8080/v1/chat/completions",
+        "url": "http://127.0.0.1:9931/v1/chat/completions",
         "toolCalling": true,
         "vision": false,
         "maxInputTokens": 6144,
@@ -1071,8 +1156,8 @@ endpoint (the host/port go here, not in the form):
 Notes: BYOK powers the chat/agent experience only (inline completions and embeddings still require a
 GitHub account). On CPU, prefer a smaller model and a modest context window — the server emits SSE
 heartbeats so a long prompt prefill does not trip the client's stream-inactivity timeout. Agent-mode
-tool calling depends on the model's own tool-calling quality. Pass `--api-key` (or
-`OpenAiServerConfig.apiKey(...)`) to require an `Authorization: Bearer` token; the server binds to
+tool calling depends on the model's own tool-calling quality. Pass `--api-key` (or `OpenAiServerConfig.builder().apiKey(...)`) to require the key, sent as
+`Authorization: Bearer ` (or as `x-api-key`, the header Anthropic clients send); the server binds to
 `127.0.0.1` by default.
 
 ### Native server with the built-in WebUI (`NativeServer`)
@@ -1087,8 +1172,8 @@ the native server (pass `--help` for the full llama-server option list):
 
 ```bash
 jbang --deps net.ladenthin:llama:6.0.0:cpu-linux-x86-64 net.ladenthin:llama:6.0.0 \
-    -m models/model.gguf --host 127.0.0.1 --port 8080 -c 65536 --jinja
-# then open http://127.0.0.1:8080/ for the WebUI
+    -m models/model.gguf --host 127.0.0.1 --port 9931 -c 65536 --jinja
+# then open http://127.0.0.1:9931/ for the WebUI
 ```
 
 Or embed it:
@@ -1096,13 +1181,13 @@ Or embed it:
 ```java
 try (NativeServer server = new NativeServer(
         "-m", "gpt-oss-20b-UD-Q4_K_XL.gguf",
-        "--host", "127.0.0.1", "--port", "8080",
+        "--host", "127.0.0.1", "--port", "9931",
         "-c", "65536", "-b", "4096", "-ub", "2048",
         "--jinja", "-ngl", "0", "-t", "8", "-tb", "16",
         "-ctk", "q8_0", "-ctv", "q8_0",
         "--chat-template-kwargs", "{\"reasoning_effort\":\"low\"}",
         "--parallel", "1").start()) {
-    // Open http://127.0.0.1:8080/ in a browser for the WebUI; the OpenAI API is at /v1/... too.
+    // Open http://127.0.0.1:9931/ in a browser for the WebUI; the OpenAI API is at /v1/... too.
     Thread.currentThread().join();
 }
 ```
@@ -1112,7 +1197,8 @@ the arguments (an independent lifecycle, like `llama-server.exe`), it is **singl
 process**, it serves the **WebUI** (in released jars — local `cmake` builds ship the empty-asset
 stub, so no UI there), and it is **not available on Android** (the upstream server needs
 `posix_spawn`). Readiness: poll `GET /health`. TLS: pass `--ssl-key-file` and `--ssl-cert-file`
-(BoringSSL is linked statically into every desktop natives jar, nothing to install). The same
+(BoringSSL is linked statically into every desktop library jar except `cpu-linux-s390x`, which ships
+without SSL; nothing to install). The same
 build fetches models from `https://` URLs (`--model-url`, `-hf`), verified against the OS
 certificate store -- on Linux `/etc/ssl/certs` or `/etc/ssl/cert.pem`, overridable with
 `SSL_CERT_FILE` / `SSL_CERT_DIR`. Note that `--model-url` alone starts the router; name the
@@ -1126,7 +1212,7 @@ JNI calls and HTTP:
 
 ```java
 try (LlamaModel model = new LlamaModel(new ModelParameters().setModel("models/model.gguf"));
-     NativeServer server = new NativeServer(model, "--host", "127.0.0.1", "--port", "8080").start()) {
+     NativeServer server = new NativeServer(model, "--host", "127.0.0.1", "--port", "9931").start()) {
     // HTTP (incl. WebUI in released jars) and direct Java calls share the same loaded model.
     String direct = model.complete(new InferenceParameters("2+2=").withNPredict(4));
     Thread.currentThread().join();
@@ -1155,7 +1241,7 @@ String javaBin = System.getProperty("java.home") + File.separator + "bin" + File
 NativeServer.setWorkerCommand(javaBin, "-cp", System.getProperty("java.class.path"),
         "net.ladenthin.llama.server.NativeServer");
 try (NativeServer router = new NativeServer(
-        "--host", "127.0.0.1", "--port", "8080", "--models-dir", "models").start()) {
+        "--host", "127.0.0.1", "--port", "9931", "--models-dir", "models").start()) {
     Thread.currentThread().join(); // each loaded model runs as a fresh worker JVM
 }
 ```
@@ -1167,16 +1253,17 @@ management endpoints, use `server.RouterClient` — a plain-HTTP typed client (w
 embedded router above or any external `llama-server` router):
 
 ```java
-RouterClient client = new RouterClient(8080);
+RouterClient client = new RouterClient(9931);
 List models = client.listModels();          // GET /models, typed status per entry
 client.loadModel("Qwen3-0.6B-Q4_K_M");                   // POST /models/load (non-blocking)
-client.awaitModelLoaded("Qwen3-0.6B-Q4_K_M", 240_000L);  // poll until LOADED; fails fast if the
+client.awaitModelLoaded("Qwen3-0.6B-Q4_K_M", 240_000L);  // poll until LOADED (or SLEEPING); fails fast if the
                                                          // worker died (exit code in the message)
 client.unloadModel("Qwen3-0.6B-Q4_K_M");                 // POST /models/unload
 ```
 
 `RouterModel` carries the identifier, the lifecycle status
-(`UNLOADED`/`LOADING`/`LOADED`/`SLEEPING`/`DOWNLOADING`/`DOWNLOADED`), the router's
+(`UNLOADED`/`LOADING`/`LOADED`/`SLEEPING`/`DOWNLOADING`/`INCOMPLETE`/`DOWNLOADED`, and `UNKNOWN` for a
+status this binding does not recognise), the router's
 failed-worker marker, and the model's input/output modalities (`getInputModalities()`,
 `getOutputModalities()`), which the router computes without loading the model: `isDecisionModel()`
 picks out a [decision model](#decision-models-v1systemone) for `/v1/systemone` before its first load.
@@ -1188,8 +1275,8 @@ Against a router started with `--api-key`, pass the key — it is sent as
 `/models/unload` were always gated, and since llama.cpp b10519 the listing endpoints are too.
 
 ```java
-RouterClient client = new RouterClient(8080, System.getenv("LLAMA_API_KEY"));
-// or, for a remote router: new RouterClient("router.internal", 8080, key)
+RouterClient client = new RouterClient(9931, System.getenv("LLAMA_API_KEY"));
+// or, for a remote router: new RouterClient("router.internal", 9931, key)
 ```
 
 > [!NOTE]
@@ -1198,14 +1285,17 @@ RouterClient client = new RouterClient(8080, System.getenv("LLAMA_API_KEY"));
 > `dedup-cache-models` still loads and still serves by name, but never appears. For those, skip the
 > await and issue the request directly; with autoload the router waits for the worker itself.
 
+`findModel` and `awaitModelLoaded` accept a model's aliases (a preset's `--alias`) as well as its id, as
+the router itself does; `RouterModel.getAliases()` lists them.
+
 ### Distributed inference over RPC
 
 llama.cpp's RPC backend spreads one model over the devices of several machines: every machine
 that contributes runs an **RPC server**, and the machine that loads the model names them with
 `--rpc`. Layers are then distributed over local and remote devices exactly as over several local
-GPUs (`setGpuLayers`, `setTensorSplit`). Both halves are in every natives jar — CPU and GPU —
-with **no additional runtime dependency** (plain TCP over the system socket
-library the library already links).
+GPUs (`setGpuLayers`, `setTensorSplit`). Both halves are in every library jar (`cpu--`,
+`metal-macos-aarch64`), so they are there whatever GPU module jars you add, with **no additional
+runtime dependency** (plain TCP over the system socket library the library already links).
 
 Serve this machine's devices (every GPU this library found, else the CPU):
 
@@ -1301,13 +1391,13 @@ try (LlamaModel llama = new LlamaModel(new ModelParameters().setModel("models/qw
 | Adapter | LangChain4j interface | java-llama.cpp call |
 |---------|-----------------------|---------------------|
 | `JllamaChatModel` | `ChatModel` | `LlamaModel.chat(...)` |
-| `JllamaStreamingChatModel` | `StreamingChatModel` | `LlamaModel.generateChat(...)` (token streaming) |
+| `JllamaStreamingChatModel` | `StreamingChatModel` | `LlamaModel.streamChatCompletion(...)` (OAI chunk streaming: text, thinking, tool calls) |
 | `JllamaEmbeddingModel` | `EmbeddingModel` | `LlamaModel.embed(...)` (model loaded with `enableEmbedding()`) |
 | `JllamaScoringModel` | `ScoringModel` (re-ranking) | `LlamaModel.handleRerank(...)` (model loaded with `enableReranking()`) |
 
 See [`llama-langchain4j/README.md`](llama-langchain4j/) for streaming/embedding/re-ranking
-examples and the current mapping limitations (tool calling, JSON mode, and multimodal input are
-not yet forwarded).
+examples and the mapped features: tool calling (blocking and streamed), JSON mode / JSON-schema structured output
+and multimodal input are forwarded; `modelName()` is ignored (one model per adapter).
 
 ### Local agent: terminal, browser, IDE via ACP
 
@@ -1334,7 +1424,7 @@ jbang net.ladenthin:llama-atmosphere-agent:6.0.0 \
     --model Qwen3-4B-Instruct-2507-Q4_K_M.gguf --workspace /path/to/project
 ```
 
-Or clone the repository and run it from that folder, which needs only JDK 21+ and Maven — the core jar from Maven Central ships the natives:
+Or clone the repository and run it from that folder, which needs only JDK 21+ and Maven — the agent's pom pulls in the CPU natives jar of every desktop platform through `llama-platform`:
 
 ```bash
 # get the folder and a tool-capable model (Qwen3-4B-Instruct-2507, 2.3 GB)
@@ -1343,7 +1433,7 @@ cd java-llama.cpp/llama-atmosphere-agent
 curl -L --create-dirs -o models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf \
   https://huggingface.co/unsloth/Qwen3-4B-Instruct-2507-GGUF/resolve/main/Qwen3-4B-Instruct-2507-Q4_K_M.gguf
 
-# the agent with the model loaded in-process — a you> prompt appears (/clear, /exit)
+# the agent with the model loaded in-process — an input prompt appears (/clear, /exit)
 mvn -q compile exec:java \
     -Dexec.args="--model models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf --ctx-size 16384 --workspace /path/to/project --allow-shell"
 ```
@@ -1372,8 +1462,9 @@ published core jars on every run (`smoke-agent-linux`) before anything is publis
 > workspace you are willing to hand to the model.
 
 In the REPL, `/help` lists the commands (`/status`, `/tools`, `/mode manual|auto`, `/compact`,
-`/clear`, `/exit`); anything else goes to the model. A status line shows the approval mode and the
-context used (`[manual · ctx ~3.1k/16k · 9 tools · local-model]`), and the answer is rendered with
+`/clear`, `/exit`); anything else goes to the model. A status line shows the workspace, the approval mode, the context used, the tool count and the model
+(`[▤ /path/to/project · ⏸  manual · ▦ ~3.1k/16k · ⚒ 9 · ◆ local-model]`; `◇` instead of `◆` for a model
+reached with `--base-url`), and the answer is rendered with
 headings, bullets and code spans.
 
 **Also in a browser or an editor.** The same agent session has two more front ends. `--web` serves it
@@ -1393,22 +1484,23 @@ jbang net.ladenthin:llama-atmosphere-agent:6.0.0 --model model.gguf --allow-shel
 On Windows PowerShell quote the whole argument (`"-Dexec.args=--model models\… --allow-shell"`); for
 the GPU add e.g. `-Dllama.classifier=vulkan-windows-x86-64` and `--ngl 99`. The agent's
 [README](llama-atmosphere-agent/) walks through all of it step by step. Other ways to run it, e.g.
-against a java-llama.cpp server that is already running (`--jinja` is required for tool calling;
+against a java-llama.cpp server that is already running (tool calling needs the Jinja chat template, which the server enables by default:
+`--jinja` below only makes that explicit, and `--no-jinja` would make it refuse tool requests;
 see [Running the server from Maven Central](#running-the-server-from-maven-central) for a GPU
 backend):
 
 ```bash
 # 1. the server, from Maven Central
 jbang --deps net.ladenthin:llama:6.0.0:cpu-linux-x86-64 net.ladenthin:llama:6.0.0 \
-    -m models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf --jinja --port 8080
+    -m models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf --jinja --port 9931
 
-# 2. the agent, from the llama-atmosphere-agent/ folder — a you> prompt appears (/clear, /exit)
+# 2. the agent, from the llama-atmosphere-agent/ folder — an input prompt appears (/clear, /exit)
 mvn -q compile exec:java \
-    -Dexec.args="--base-url http://127.0.0.1:8080/v1 --workspace /path/to/project --allow-shell"
+    -Dexec.args="--base-url http://127.0.0.1:9931/v1 --workspace /path/to/project --allow-shell"
 
 # a single turn instead of the prompt loop
 mvn -q compile exec:java \
-    -Dexec.args="--base-url http://127.0.0.1:8080/v1 --workspace /path/to/project --prompt 'Read the README and summarize it'"
+    -Dexec.args="--base-url http://127.0.0.1:9931/v1 --workspace /path/to/project --prompt 'Read the README and summarize it'"
 
 # or without a separate server: load the GGUF in-process
 mvn -q compile exec:java \
@@ -1456,11 +1548,11 @@ library wraps it in a few lines without `java-llama.cpp` pulling in a runtime
 reactive dependency.
 
 **Always wrap with the library's resource-management primitive** — `Flux.using`,
-`Flowable.using`, Kotlin `use {}`, etc. — so that subscription cancellation
-flows into `LlamaIterable.close()` and from there into llama.cpp's native
-`cancelCompletion`. A plain `Flux.fromIterable(iterable)` or `for (x in iter)`
-loop will NOT close the iterable on cancel; the native task slot stays
-occupied until the model is closed.
+`Flowable.using`, Kotlin `use {}`, etc. — so that subscription cancellation flows into `LlamaIterable.close()`, which releases the task's
+native reader and so cancels the generation in llama.cpp's server. A plain
+`Flux.fromIterable(iterable)` or `for (x in iter)` loop will NOT close the iterable on cancel: the
+generation keeps its slot until it stops on its own (e.g. `n_predict`, an end-of-generation token
+or the end of the context), and its undelivered results stay queued until the model is closed.
 
 #### Project Reactor (Spring WebFlux)
 ```java
@@ -1483,8 +1575,9 @@ Flowable tokens = Flowable.using(
 #### Kotlin Flow (Android / coroutines)
 
 Ready-made: the optional [`net.ladenthin:llama-kotlin`](llama-kotlin/README.md) artifact ships
-`generateFlow`/`generateChatFlow` extensions (close-on-cancellation included) plus `suspend`
-wrappers whose coroutine cancellation is wired to the binding's cooperative `CancellationToken`:
+`generateFlow`/`generateChatFlow` extensions (close-on-cancellation included) plus `suspend` wrappers (`completeSuspend`, `chatSuspend`, `chatCompleteTextSuspend`, `embedSuspend`).
+Only `completeSuspend` wires coroutine cancellation to the binding's cooperative `CancellationToken`;
+the other three resume only after the native call has returned:
 
 ```kotlin
 model.generateChatFlow(params).flowOn(Dispatchers.IO).collect { print(it.text) }
@@ -1501,14 +1594,16 @@ fun llama(model: LlamaModel, params: InferenceParameters) = flow {
 The companion Android sample [LLaMAndroid](https://github.com/Rattlyy/LLaMAndroid)
 demonstrates the `flow { for (output in model.generate(params)) emit(output) }`
 shape against the upstream binding. Wrap the `for` loop in
-`.use { }` if your collector may cancel mid-stream — otherwise the native task
-slot will not be released until the model is closed.
+`.use { }` if your collector may cancel mid-stream — otherwise the generation keeps its slot until its natural stop, and its undelivered results stay
+queued until the model is closed.
 
 #### Akka Streams
 ```scala
 val tokens: Source[LlamaOutput, NotUsed] = Source
-    .fromIterator(() => model.generate(params).iterator())
-    .async("blocking-io-dispatcher")
+    .unfoldResource[LlamaOutput, LlamaIterator](
+        () => model.generate(params).iterator(),
+        it => if (it.hasNext) Some(it.next()) else None,
+        _.close()) // Akka's blocking-IO dispatcher by default; closes the iterator on completion, failure and cancellation
 ```
 
 **Why no built-in `Publisher`?** Earlier snapshots of this fork shipped a
@@ -1522,7 +1617,8 @@ app already uses. The pattern is verified end-to-end by
 ### Logging
 
 Per default, llama.cpp writes its log as text to **stderr** (`0.00.035.060 I slot …` once a model is
-loaded): the server's own `srv …` / `slot …` lines and, from verbosity 4 on, the llama/ggml lines.
+loaded): the server's own `srv …` / `slot …` lines, the llama/ggml warnings and errors and, from
+verbosity 4 on, the llama/ggml info lines as well.
 All of it can be intercepted via the static method
 `LlamaModel.setLogger(LogFormat, BiConsumer)`: with a callback set, every line goes
 to the callback instead of the console (a `setLogFile` file keeps receiving them). The callback
@@ -1577,13 +1673,15 @@ dependencies {
 ```
 
 The AAR carries the full `net.ladenthin:llama` Java API, the CI-built native library with ggml's
-CPU backend modules (one per ARM feature level, the best for the device loaded at start) for
+CPU backend modules (one per instruction-set level, ARM feature levels on `arm64-v8a` and x86
+levels on `x86_64`, the best for the device loaded at start) for
 `arm64-v8a` (devices) **and** `x86_64` (Android Studio emulator, Chromebooks — app bundles
 split per ABI so phones download only arm64), all 16 KB page-size compliant, consumer
 R8/ProGuard rules (applied automatically), and a manifest `minSdkVersion 28` that AGP
 enforces against your app. The OpenCL AAR adds the `libggml-opencl.so` module, which the same
 start loads when the device has an OpenCL ICD and skips otherwise. CI boots an x86_64 emulator
-and runs real on-device inference against every AAR build.
+and runs real on-device inference against the CPU AAR's `x86_64` libraries on every run; the
+`arm64-v8a` libraries and the OpenCL AAR are checked for structure and dependencies, not run on a device.
 Do **not** also depend on the desktop `net.ladenthin:llama` JAR in the same app — the AAR
 already contains those classes (the JAR carries no natives since 6.0.0, it would only duplicate
 every class). See [`llama-android/README.md`](llama-android/README.md) and
@@ -1612,12 +1710,8 @@ android {
 
     // Execute "mvn compile" in the llama/ core module if its target/ doesn't exist
     // (the repository root is the Maven reactor aggregator; the native core lives in llama/).
-    if (!file("$jllamaLib/llama/target").exists()) {
-        exec {
-            commandLine = listOf("mvn", "compile")
-            workingDir = file("java-llama.cpp/llama/")
-        }
-    }
+    // No "mvn compile" step is needed: under AGP, CMake takes OS_NAME/OS_ARCH from ANDROID_ABI,
+    // and the JNI header (llama/src/main/cpp/jllama.h) is committed.
 
     ...
     defaultConfig {
@@ -1634,7 +1728,7 @@ android {
     // Declare c++ sources
     externalNativeBuild {
         cmake {
-            path = file("$jllamaLib/CMakeLists.txt")
+            path = file("$jllamaLib/llama/CMakeLists.txt")
             version = "3.22.1"
         }
     }
@@ -1643,28 +1737,30 @@ android {
     sourceSets {
         named("main") {
             // Add source directory for java-llama.cpp
-            java.srcDir("$jllamaLib/src/main/java")
+            java.srcDir("$jllamaLib/llama/src/main/java")
         }
     }
 }
 ```
-3. Exclude `net.ladenthin.llama` in proguard-rules.pro
+The sources need the core's compile dependencies in the app's `dependencies { }`: `com.fasterxml.jackson.core:jackson-databind`, `org.slf4j:slf4j-api` and `org.jspecify:jspecify`, plus `org.projectlombok:lombok` as `compileOnly` + `annotationProcessor` and `org.codehaus.mojo:animal-sniffer-annotations` as `compileOnly` (versions in `pom.xml` and `llama/pom.xml`). Three files of `llama/src/main/java` do not compile against `android.jar` and have to be excluded from the source set: `module-info.java`, `net/ladenthin/llama/server/OpenAiCompatServer.java` (the JDK's `com.sun.net.httpserver`) and `net/ladenthin/llama/server/ServerLauncher.java`, which calls it. The AAR of Option 1 avoids all of this: it ships the classes jar Maven compiled.
+
+3. Exclude `net.ladenthin.llama` in proguard-rules.pro (the full rule set the AAR ships is [`llama-android/consumer-proguard.txt`](llama-android/consumer-proguard.txt))
 ```proguard
-keep class net.ladenthin.llama.** { *; }
+-keep class net.ladenthin.llama.** { *; }
 ```
 
 ## TODO
 
 Open work items live in [`TODO.md`](TODO.md).
 
-- **Expand PIT mutation-testing scope.** PIT is wired in `pom.xml` and runs on every CI build (in the `test-java-linux-x86_64` job) with `100`. `` currently covers `net.ladenthin.llama.value.*`, `exception.*`, `args.*` and four `json` parsers (295 mutations, 100% killed, hermetic — no model or fixture needed); widen it incrementally as additional classes reach mutation-test parity. Final target: `net.ladenthin.llama.*` matching the streambuffer pattern.
+- **Expand PIT mutation-testing scope.** PIT is wired in `llama/pom.xml` and runs on every CI build (in the `test-java-linux-x86_64` job) with `100`. `` currently covers `net.ladenthin.llama.value.*`, `exception.*`, `args.*`, four `json` classes (`TimingsLogger`, `RerankResponseParser`, `ChatResponseParser`, `CompletionResponseParser`) and `parameters.JsonParameters` (100% killed, hermetic — no model or fixture needed); widen it incrementally as additional classes reach mutation-test parity. The rest of the `parameters` package (one-line builder setters) is deliberately kept off the gate.
 
 ## Feature Ideas
 
 Forward-looking ideas being tracked for this fork:
 
 - **Adopt feature ideas from the Kotlin Llama Stack client.** Candidates (multimodal image input, typed chat messages, async API, batch inference, typed usage/timings) are inventoried with effort estimates in [`docs/feature-investigation-llama-stack-client-kotlin.md`](docs/feature-investigation-llama-stack-client-kotlin.md), derived from [`ogx-ai/llama-stack-client-kotlin`](https://github.com/ogx-ai/llama-stack-client-kotlin).
-- **Ship a directly Android-capable artifact — DONE.** `net.ladenthin:llama-android` / `llama-android-opencl` (AAR, arm64-v8a, minSdk 28, consumer ProGuard rules, 16 KB page-size compliant) plus the optional `net.ladenthin:llama-kotlin` coroutines façade ship from this repo — see [Importing in Android](#importing-in-android). Typed image input for VLMs is covered by `ContentPart.imageBytes(...)` / `imageFile(...)` (see the multimodal section), so downstream Android projects can drop their dependency on [`ogx-ai/llama-stack-client-kotlin`](https://github.com/ogx-ai/llama-stack-client-kotlin) entirely. A dedicated KISS example app — **"LLM Service"** (SAF model picker + Compose streaming chat, 13-language flag picker, private local save/load, plain Gradle/AGP, signed `.aab`, on-device emulator UI test) — ships in [`android-llmservice/`](android-llmservice/README.md).
+- **Ship a directly Android-capable artifact — DONE.** `net.ladenthin:llama-android` / `llama-android-opencl` (AARs: the CPU one for arm64-v8a and x86_64, the OpenCL one for arm64-v8a; minSdk 28, consumer ProGuard rules, 16 KB page-size compliant) plus the optional `net.ladenthin:llama-kotlin` coroutines façade ship from this repo — see [Importing in Android](#importing-in-android). Typed image input for VLMs is covered by `ContentPart.imageBytes(...)` / `imageFile(...)` (see the multimodal section), so downstream Android projects can drop their dependency on [`ogx-ai/llama-stack-client-kotlin`](https://github.com/ogx-ai/llama-stack-client-kotlin) entirely. A dedicated KISS example app — **"LLM Service"** (SAF model picker + Compose streaming chat, 13-language flag picker, private local save/load, plain Gradle/AGP, signed `.aab`, on-device emulator UI test) — ships in [`android-llmservice/`](android-llmservice/README.md).
 - **Resolve all upstream `kherud/java-llama.cpp` open issues.** All 37 open issues at fork time are catalogued with per-issue verdicts in [`docs/history/49be664_open_issues.md`](docs/history/49be664_open_issues.md); fixes land in this fork as they are completed. Vision inputs (issues [#103](docs/history/49be664_open_issues.md#103--vlm-support--image-input-for-multimodal-models) and [#34](docs/history/49be664_open_issues.md#34--support-multimodal-inputs)) are now wired end to end through blocking, typed, streaming, and OpenAI-compatible request surfaces.
 
 ## Troubleshooting
@@ -1682,10 +1778,11 @@ C [msvcp140.dll+0x12f58]
 `msvcp140.dll` from your JDK — is obsolete. Do not do it.** The Windows natives link the C++
 standard library and vcruntime statically (hybrid CRT), so they do not import `msvcp140.dll`,
 `vcruntime140.dll` or `vcruntime140_1.dll` at all; the only non-OS import left is the Universal
-CRT, which Windows itself provides and keeps updated. Measured on the current build, the whole
-import table of `jllama.dll` is `KERNEL32`, `ADVAPI32`, `SHELL32`, `WS2_32` and the
-`api-ms-win-crt-*` forwarders. CI enforces that per release
-(`.github/buildcheck/nativedeps.py` holds every Windows CPU directory to an exact allowlist).
+CRT, which Windows itself provides and keeps updated. The import table of `jllama.dll` is `KERNEL32`, `ADVAPI32`, `SHELL32`, `WS2_32`, `CRYPT32`
+(cpp-httplib's certificate-store lookup for HTTPS), the `api-ms-win-crt-*` forwarders, and the
+`ggml-base.dll`, `ggml.dll`, `llama.dll` and `mtmd.dll` shipped next to it. CI enforces that per
+release (`.github/buildcheck/nativedeps.py` holds every Windows CPU directory to an exact allowlist
+of those OS DLLs plus the files next to the library).
 
 The report dates from before that: the troubleshooting note was written on 2026-04-04 and the
 switch from the DLL runtime (`/MD`) to a static one landed on 2026-05-13, so releases from
@@ -1731,7 +1828,7 @@ the same tree; `mvn -version` shows which JDK Maven runs on.
 
 ### Contributors: do not upgrade jqwik past 1.9.3
 
-> ⚠️ **DO NOT UPGRADE jqwik past 1.9.3.** jqwik 1.10.0 added an anti-AI prompt-injection string to test stdout; the 1.10.1 user guide states the library "is not meant to be used by any 'AI' coding agents at all." 1.9.3 is the last pre-disclosure release and is the pinned version. See `CLAUDE.md` section "jqwik prompt-injection in test output" for the full context. Dependabot is configured to ignore **all** `net.jqwik` updates (every version, including patches) — see the `ignore` rule in [`.github/dependabot.yml`](./.github/dependabot.yml).
+> ⚠️ **DO NOT UPGRADE jqwik past 1.9.3.** jqwik 1.10.0 added an anti-AI prompt-injection string to test stdout; the 1.10.1 user guide states the library "is not meant to be used by any 'AI' coding agents at all." 1.9.3 is the last pre-disclosure release and is the pinned version. See the "jqwik Policy" section of `CLAUDE.md`, which points to the cross-repo policy `workspace/policies/jqwik-prompt-injection.md`, for the full context. Dependabot is configured to ignore **all** `net.jqwik` updates (every version, including patches) — see the `ignore` rule in [`.github/dependabot.yml`](./.github/dependabot.yml).
 
 ## Similar Projects / Usage
 
diff --git a/REUSE.toml b/REUSE.toml
index 44fcba61b..87f23ded6 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -77,19 +77,6 @@ path = "llama/patches/**"
 SPDX-FileCopyrightText = "2026 Bernard Ladenthin "
 SPDX-License-Identifier = "MIT"
 
-# The Kolibri-1 patch derives from two community ports (see its header): the llama.cpp-derived
-# parts are MIT, Eliasfpv28's additions Apache-2.0. Aleph Alpha's Apache-2.0 reference served as
-# the specification; no code of it is copied.
-[[annotations]]
-path = "llama/patches/0016-model-kolibri1.patch"
-precedence = "override"
-SPDX-FileCopyrightText = [
-    "2026 Bernard Ladenthin ",
-    "2023-2026 The ggml authors",
-    "2026 Eliasfpv28 (Kolibri1 llama.cpp port)",
-]
-SPDX-License-Identifier = "MIT AND Apache-2.0"
-
 # Project logo / branding assets (binary + generated SVG, cannot carry inline SPDX
 # without corrupting the base64-embedded font). Same MIT OR Apache-2.0 dual license
 # the author ships them under in the workspace repo (the embedded Martian Mono font
diff --git a/SECURITY.md b/SECURITY.md
index 360e7792e..31445f97a 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -39,8 +39,8 @@ Default embargo: **90 days** from the date the report is acknowledged, or until
 **In scope:**
 
 - The Java API surface (`net.ladenthin.llama.*`).
-- The project's JNI layer (`src/main/cpp/jllama.cpp`, `utils.hpp`, `json_helpers.hpp`, `jni_helpers.hpp`).
-- The project's native build glue (`CMakeLists.txt`, `.github/build_*.sh`, dockcross scripts) — including issues that result in unsafe linkage, missing hardening flags, or supply-chain risk in our build pipeline.
+- The project's native layer under `llama/src/main/cpp/`: the JNI shells (`jllama.cpp`, `native_server.cpp`, `rpc_bridge.cpp`), the C API (`jllama_api.h`/`.cpp`) and the engine behind both (`jllama_engine.cpp`, the helper headers), and the llama.cpp patches this project carries (`llama/patches/`).
+- The project's native build glue (`llama/CMakeLists.txt`, `llama/cmake/`, `.github/build*.sh`, dockcross scripts) — including issues that result in unsafe linkage, missing hardening flags, or supply-chain risk in our build pipeline.
 - Maven Central artefact integrity (signing, publishing workflow).
 
 **Out of scope:**
diff --git a/TODO.md b/TODO.md
index 3a440c34c..a175992be 100644
--- a/TODO.md
+++ b/TODO.md
@@ -78,10 +78,12 @@ each vendor publishes next to the file.)* Still open, each with the reason:
       measured hash so it can be pinned after a check on a trusted machine instead of costing another
       run; and the CUDA archive names come from the manifest (`relative_path`) instead of upstream's
       hand-written list, whose `cuda_crt 13.4.59` was not the `cuda_crt` the newest manifest hashes.
-- [ ] **The oneAPI Windows installer has no checksum endpoint** Intel publishes next to the file
-      (`registrationcenter-download.intel.com/.../intel-deep-learning-essentials-2025.3.3.18_offline.exe`);
-      pinning needs one download of the 1.6 GB installer on a machine that can reach it, then
-      `Get-FileHash` into a `ONEAPI_INSTALLER_SHA256` check like the Vulkan one.
+- [ ] **The oneAPI installers have no checksum endpoint** Intel publishes next to the files (since the b11800
+      bump both SYCL jobs take upstream's: Deep Learning Essentials 2026.1.4 and oneDNN 2026.0.2 for Linux
+      and Windows, `ONEAPI_DLE_URL` / `ONEAPI_ONEDNN_URL` in `publish.yml`); pinning needs one download of
+      each installer on a machine that can reach `registrationcenter-download.intel.com`, then `sha256sum` /
+      `Get-FileHash` into a check like the Vulkan one. The Level Zero debs and SDK zip come from a GitHub
+      release and could be hashed the same way.
 - [ ] **ROCm wheels come from a moving index** (`stable.repo.amd.com/rocm/whl-next/`, version-pinned
       with `==` but not hashed): `pip install --require-hashes` needs a requirements file with the
       hash of every wheel of `rocm[libraries,devel]`, which only a machine that can download the
@@ -216,9 +218,13 @@ only under that environment's rules -- never a step of `package`, which runs on
 *(The code side landed 2026-10-15: `.github/sign-windows-natives.sh` (jsign 7.2 pinned by SHA-256,
 `--storetype ESIGNER`, every `Windows/*/*/*.dll`, `signed=yes` afterwards; measured with a real DLL
 and dummy credentials: the options are accepted and the run fails at SSL.com's authentication), the
-job `sign-windows-natives` (environment `maven-central`, re-uploads the ten Windows artifacts with
-`overwrite: true`, `check-natives.py` holds the uploads to the Windows rows of `natives.csv`),
-`package` waiting for it with a skip-tolerant `if`, `--require-signed` in `package` and both publish
+job `sign-windows-natives` (environment `maven-central` only when signing, else the unprotected
+`unsigned`; re-uploads the ten Windows artifacts with `overwrite: true`, `check-natives.py` holds
+the uploads to the Windows rows of `natives.csv`), `package` waiting for it -- the job runs on every
+run and is never `if:`-skipped, because GitHub applies a skip to the whole dependency chain behind
+the skipped job past any condition in between, which is how run 38870889665 (attempt 2) ended
+"success" with every job behind `package` skipped; `check-natives.py` refuses a job-level `if:` on
+it now --, `--require-signed` in `package` and both publish
 jobs whenever `SIGN_WINDOWS` is on, `pe_signed()` + `signature_violations()` in
 `verify-native-deps.py` with unit tests; CLAUDE.md "Code signing of the Windows DLLs", README.
 Unexercised until the certificate exists.)*
@@ -258,32 +264,14 @@ them to ggml's `if (NOT MSVC)` block). All 14 x86 modules were measured on one G
 and 6 of 8 aarch64 ones on a Graviton 4 -- before that, no module had ever been *chosen* on hardware
 the project could inspect.
 
-**Where it sticks, in the order that matters for a consumer:**
-
-1. **The library jar is not self-contained** (item 1 below). Java runs in containers, and the usual
-   slim base images have no `libgomp`; the jar then fails to load at all. Measured, decided, not yet
-   changed.
-2. **"A module that cannot help is skipped" holds for Vulkan and breaks for the other two**
-   (items 2 and 3): CUDA on an incompatible driver `abort()`s the JVM in *every* configuration,
-   OpenVINO on a CPU-only host takes over and runs 172x slower. The premise the whole additive design
-   rests on is therefore conditional, and that was unknown until a module was actually used.
-3. **Nothing behind `package` has ever run on the modular natives.** `package` was red until the
-   Windows OpenVINO module jar was unblocked, so the smoke-set assembly, `smoke-natives-jars.sh` with
-   all 23 jars on classpath *and* module path, the four `smoke-natives` rows, the macOS smoke, the
-   agent smoke, the bytecode gate and `verify-native-deps.py` over all 23 jars have never executed
-   together. **This is what the next Publish run is for.**
-
-**So the next CI run should be the current `main`, unchanged.** It is the first opportunity to see
-everything behind `package`, and bundling the decided changes into it would confound the result: a red
-run would not say whether the pipeline or the change is at fault. Expect several rounds there -- a
-stage that has never run is unlikely to pass first time -- and note that the `windows-x86-64` smoke
-row draws the same CPU lottery as the Windows test job, so it exercises AMX only when GitHub hands it
-an Intel runner.
-
-**The batch for the run after that**, assembled while the first one runs: `GGML_OPENMP=OFF` plus
-`-static-libstdc++ -static-libgcc` on Linux and s390x with the dependency allowlist inverted, the
-explicit `CMAKE_CUDA_ARCHITECTURES`, the OpenVINO opt-in decision, and the shipped diagnostic entry
-point (item 5). These are independent of each other and each is verifiable in the same run.
+**What stuck after the first runs, and where it went** (kept so it is not re-derived): the library
+jar was not self-contained (`libgomp` missing on slim images) -- Linux builds with `GGML_OPENMP=OFF`
+now and `verify-native-deps.py` holds the absence, while `libstdc++`/`libgcc_s` stay dynamic on
+purpose (CLAUDE.md "Windows natives", the Linux OpenMP paragraph); CUDA aborted the JVM on an old
+driver -- the explicit `CMAKE_CUDA_ARCHITECTURES` fixed it (T4 measured); OpenVINO on a CPU-only host
+took over and ran 172x slower -- it is opt-in (`LlamaLoader.OPT_IN_BACKENDS`); nothing behind
+`package` had ever run -- it has, in the two green runs above; and the shipped diagnostic entry point
+is `net.ladenthin.llama.Diagnose`. The rounds that got there follow.
 
 ### Round 0 found one defect, and it was mine
 
@@ -507,6 +495,127 @@ Kept as a list because each entry names the machine it needs -- the harness is
 
 ## Open — jllama-specific
 
+### Open points from the bug hunt of 2026-10-16 … 19 (details: `BUGHUNT-FOUND.md`)
+
+Everything the bug hunt left open, so nothing is lost when the session ends. The ledger entry
+named in each item holds the analysis, the upstream references (b11639) and the reproduction;
+delete an item here when it is done.
+
+**Open suspicions without a finding id**: none left -- the last ones, numbered (1) to (16) while they
+were open (comments and ledger entries still cite those numbers), were fixed as F181 to F186 on
+2026-10-20. The local measurement run of that brief
+(`BUGHUNT-MEASUREMENTS.md`, M1 … M10, Windows) confirmed all of them with the real models and found
+four more, fixed as F187 … F190. It left limits that are decided and documented, recorded here so
+they are not rediscovered:
+
+- **`--lora-scaled` / `--control-vector-scaled` cannot carry an absolute Windows path** (F187).
+  Upstream reads `FNAME:SCALE,...` and `string_split` splits on *every* colon, so `X:\a.gguf:1.0`
+  is three parts. `ModelParameters` refuses such a path with the reason and the two ways out (a
+  relative path, or load unscaled and call `LlamaModel.setLoraAdapter`). A candidate for the upstream
+  inventory below (split on the last colon); nothing to do downstream.
+- **The embedded `OpenAiCompatServer(LlamaModel, config)` reports the model's loaded name, not
+  `config.getModelId()`** (F189). An already-loaded model cannot take `--alias` any more; the Javadoc
+  of `OpenAiServerConfig.getModelId()` tells an embedding caller to load with
+  `ModelParameters.setAlias(…)`. Rewriting the `model` field of every answer was rejected (it would
+  claim something the engine did not do, and attach-mode `NativeServer` could not follow it).
+- **`OpenAiCompatServer` cannot unload a model**: an Ollama `/api/generate` with `keep_alive: 0`
+  and no prompt gets the "load" answer (F205), the model stays resident (one model per server).
+  Ollama itself unloads there and answers `done_reason "unload"`; this server does not claim an
+  unload it did not do.
+
+### Open points from the branch review of 2026-10-20/21 (details: `BUGHUNT-FOUND.md`)
+
+The review of the bug-hunt branch confirmed its findings; the fixes are F213 … F267 and the dated
+addenda in the ledger (F250 … F267 closed every open point of this list on 2026-10-21, before
+6.0.0, by the owner's decision). What is still open, or what the reviewers raised outside the
+findings' scope, is listed here; the ledger entry in brackets holds the analysis. Delete an item when
+it is done.
+
+**Anthropic Messages**
+- **Upstream candidate:** `stop_reason` `stop_sequence` for a matched stop word (F221); recorded in
+  the upstream inventory ("Upstream submissions", last bullet) for the day after the release.
+
+**Decided deviations, recorded so they are not rediscovered** (no action): unknown part types inside
+a Responses `message` are not refused, because the same branch carries assistant `output_text` /
+`refusal` parts (F224); upstream's stricter Responses role checks are not adopted (F228); a boolean or
+string rerank `top_n` and one outside the `int` range keep every result, where upstream converts
+`true` to 1 and wraps the cast (F230); a `documents` of the wrong type stays `400` rather than
+falling back to `texts` (F231); `chat_template_kwargs` keeps its string fallback, because a string
+literal is a valid kwarg value while a tool schema must be an object (F233); a stop issued before
+the serving thread has entered `jllama_rpc_server_serve` cannot be told from "nothing running" and
+stays a no-op -- callers repeat the stop until the serve returns, as `RpcServer` does (F241).
+
+**Tests**
+- **The model-backed rerank tests were not run locally** (no reranker model in the sandbox):
+  `OpenAiServerRerankIntegrationTest` covers F230 … F232 end to end only in CI, and F177's caveat that
+  its end-to-end case had not run locally can go once a CI run with the jina reranker shows it green.
+- **Proven only by the first CI run with the real models** (the closing pass ran each touched class
+  against the random two-layer test model, whose 128-token context and missing special tokens cannot
+  carry them): the text assertion F250 added to
+  `ChatAdvancedTest#testReasoningBudgetBesideMessagesInCompleteAndGenerateForcesTheEndTag` can
+  only be red with Qwen3, whose `` is a special token -- the C++ case
+  `CApiReasoningBudget.ARawCompletionWithMessagesRendersTheTemplatesSpecialTagsAsText` pins the
+  behaviour on every platform; `ChatScenarioTest#testChatCompleteMultiTurnThreeTurns` and the
+  `Session` tests ran locally only with a 4096-token variant of the test model;
+  `MultimodalIntegrationTest#imageBytesAffectTheAnswer` needs SmolVLM; and whether
+  `llama-atmosphere-agent` and `llama-langchain4j` ever send `OpenAiCompatServer` a `tool_choice`
+  object of a type other than `function` or a number as a string (F254, F253 refuse both now) --
+  their sources send string `tool_choice` values only, their integration jobs are the proof.
+- **`ErrorHandlingTest#testSaveSlotWithoutSlotSavePathIsRefused` is not hermetic** (F251): it checks
+  that `slot.bin` is absent from the working directory, so a file left there by a run of an older
+  library fails it. Deterministic against the fixed library; a temporary working directory, or
+  asserting the native answer alone, would make it independent of what lies around.
+- **A custom `OpenAiBackend` without `mediaMarker()` answers `/api/generate` with images `500`**
+  (F259): the default method throws `UnsupportedOperationException`, which the route does not map to
+  `501` as `countTokens` is. `LlamaModelBackend` implements it, so the shipped backend is not
+  affected; a `501` with the reason would match the other optional capabilities.
+
+### After the llama.cpp bump to b11800 (2026-10-21): what the range offers and is not wired yet
+
+The bump itself is done (b11639 -> b11760 -> b11761 -> b11799 -> b11800, b11803 on 2026-10-21: Flint, SenseNova
+U1 and a jinja fix, nothing to wire; b11809 on 2026-10-22: LoRA on prism.hadamard folded weights, an OpenCL
+ARGMAX kernel, nothing to wire; b11814 on 2026-10-22: the `tokenizer.ggml.clean_spaces` key and the Pixtral
+pre-tokenizer change, nothing to wire; b11815 on 2026-10-22: a SYCL CONV_2D layout fix, nothing to wire; patches `0016` and `0019` dropped, `0007` refreshed;
+`docs/history/llama-cpp-breaking-changes.md` has the rows). Four things the range brings that this
+library does not expose, each a decision rather than an omission:
+
+- **Server-side TTS through `LlamaModel` / the Java transport.** `POST /tts` (b11781, #26603) runs
+  `SERVER_TASK_TYPE_TTS` on a chat model loaded with a TTS mmproj and streams `server_task_result_tts` chunks
+  (`audio`, `sample_rate`, `final`; WAV or PCM per `out_type`). `NativeServer` serves it already (classic and
+  attach mode, the route is in `0007`'s common table). A `LlamaModel.tts(...)` over the C API and a
+  `/v1/audio/speech`-shaped route on `OpenAiCompatServer` would make it reachable without the native
+  frontend; `TextToSpeech` (its own context over `mtmd_helper::gen_audio`) stays the one-shot path. Not before
+  the release.
+- **Realtime transcription** (`/v1/realtime`, WebSocket, b11780, #30636: `mtmd_audio_stream`,
+  `SERVER_TASK_TYPE_TRANSCRIBE_RT`, `SERVER_CONTROL_ACTION_AUDIO_*`). Served by `NativeServer`; the JDK's
+  `com.sun.net.httpserver` has no WebSocket, so the Java transport cannot follow without a dependency. Not
+  planned.
+- **`server_context_meta::has_cap_chat` / `has_cap_tts` / `has_audio_stream`** are not in the meta JSON
+  (`ModelMeta` shows `"audio"` among the output modalities for a TTS-capable model, which covers the second).
+  Add a `capabilities` object when the first item lands; `value.*` is PIT-gated at 100 %, so each getter
+  needs its test.
+- **`--connect` / `--connect-code`** (b11744, #28277): the server spawns the separate `llama-connect`
+  binary (WebRTC tunnel to a browser outside the LAN). `LLAMA_CONNECT` stays OFF (a prebuilt download at
+  configure time), `tools/server/server-connect.cpp` is compiled because `server.cpp` references it, and the
+  flag answers with upstream's "not available" message. No plan to bundle a binary.
+
+**The SYCL jobs follow upstream's installers since this bump** (owner's ask, 2026-10-21): Deep Learning
+Essentials 2026.1.4 + oneDNN 2026.0.2 offline installers on both OSes (upstream since b11654, #30131), pinned by
+URL under `KEEP IN SYNC WITH UPSTREAM`, instead of Intel's unpinned apt packages (Linux) and the 2025.3.3 bundle
+(Windows); and the Level Zero loader/SDK upstream's jobs have carried since #21597 / #29273. **That last part is
+a change to the shipped modules, not only to the toolchain:** the modules built so far never found a Level Zero
+loader (the package job of run 38870889665 listed none among their imports), so from this bump on
+`libggml-sycl.so` imports `libze_loader.so.1` and `ggml-sycl.dll` imports `ze_loader.dll` (Intel's GPU driver
+ships both) and both carry ggml-sycl's Level Zero allocation path; and the Windows module moves from the oneAPI
+2025.3 runtime (`sycl8.dll`, `mkl_sycl_blas.5.dll`) to 2026.1 (`sycl9.dll`, `mkl_sycl_blas.6.dll`). CHANGELOG
+and the README classifier table say so. **Unverified here** -- Intel's download host answers this sandbox with
+403, and no job runs locally -- so the first Publish run proves it; what to read there: both installers download
+and install silently, `setvars.sh` / `setvars.bat` exist, the configure prints `Level Zero loader found` and no
+`oneDNN not found` FATAL_ERROR, and the package job's dependency lines show `libze_loader.so.1` / `ze_loader.dll`
+and `sycl9.dll` (`verify-native-deps.py` holds a GPU directory to a denylist only, so a vendor loader passes). The OpenSSL half of upstream's change (b11676, #30456: the
+Linux job builds against `libssl-dev`) does not apply: the SYCL jars are module-only builds without
+cpp-httplib, and a shipped `libjllama` must not import a dynamic `libssl` anyway.
+
 ### The release: 6.0.0, with an "Upgrading from 5.1" section (decided 2026-10-15; roadmap item 9)
 
 **Nothing has been published since 5.1.0 on 2026-08-29.** `pom.xml` is `6.0.0-SNAPSHOT`, and
@@ -522,8 +631,9 @@ from 5.1 would look:
   load with `UnsatisfiedLinkError`;
 - `slf4j-simple` instead of `logback-classic` as the shipped binding; the ROCm jars on ROCm 10; the
   `LazyMode` rename and the `enableFlashAttn()` rework; twelve dead builder methods removed;
-  `NativeServer` on port 9931 (llama.cpp's new default) while `OpenAiServerConfig` stays on 8080
-  (open item below); the floors Windows 10, macOS 15 and glibc 2.28.
+  both servers on port 9931 (llama.cpp's new default; `OpenAiServerConfig` moved from 8080 on
+  2026-10-21); slot files named inside `--slot-save-path`; the floors Windows 10, macOS 15 and
+  glibc 2.28.
 
 Semantic versioning says that is **6.0.0**, not 5.2.0, and the owner decided so on 2026-10-15; the
 timing of the tag stays the owner's. What the release needs, in order:
@@ -531,16 +641,19 @@ timing of the tag stays the owner's. What the release needs, in order:
 1. **Round 0 green through `package` and the smokes** -- the modular natives have never passed the
    stages that assemble and launch the published jars (top of this file); a release before that
    ships jars CI has not loaded, which the smoke jobs exist to prevent.
-2. **An "Upgrading from 5.1" section at the top of the release's changelog entry**: the dependency
-   change (`llama-platform` as `pom`, or the classifier jars by name, plus a GPU module jar where
-   wanted), the logging binding, the port, the floors, the renamed and removed builder methods -- one
-   place, written for someone who reads nothing else. The `[Unreleased]` bullets stay as the record.
+2. ~~**An "Upgrading from 5.1" section at the top of the release's changelog entry**~~ -- **done**
+   (`CHANGELOG.md`, `[Unreleased]`, "Upgrading from 5.1", 18 items, completed 2026-10-21 with the
+   closing pass): the dependency change, the logging binding, the port, the floors, the renamed and
+   removed builder methods, the requests refused instead of dropped, the slot file names, the loader
+   and the stream envelope -- one place, written for someone who reads nothing else. The
+   `[Unreleased]` bullets stay as the record. Keep it current until the tag: a breaking change that
+   lands later needs its item there too.
 3. **The version in every place the bump touches**: `mvn versions:set` for the five reactor poms,
    then by hand the two READMEs' snippets, `examples/jbang/Chat.java`, `examples/server/pom.xml`,
    `llama-atmosphere-agent/pom.xml` and its READMEs, the Android and Kotlin READMEs (CLAUDE.md
-   "Version bump" lists them; `check-natives.py` fails on the ones it can see), then the CHANGELOG
-   heading and footer links, which `../workspace/workflows/release-process.md` names as the
-   most-missed step.
+   "Version bump" lists them; `check-natives.py` fails on the ones it can see), the supported-versions
+   table of `SECURITY.md` (it names `5.x` as the supported line), then the CHANGELOG heading and
+   footer links, which `../workspace/workflows/release-process.md` names as the most-missed step.
 4. After the release: the upstream submissions (policy, below) and the `main` bump back to a
    `-SNAPSHOT` of the next version.
 
@@ -1151,12 +1264,14 @@ three different subsystems and a combined report gets triaged as one.
 | 3 | `ggml-rpc: the client answers every supports_op with true` | the RPC section of `CLAUDE.md`: a served device that lacks an op **aborts the server**. Reproduced on the macOS CI runners' paravirtual Metal GPU, which has no `MUL_MAT` and took the first run down; it is why every test here serves `CPU` by name | upstream's own TODO comment admits it. Should the client forward the query to the server, or should `rpc-server -d` refuse a device whose op set it cannot report? |
 | 4 | `windows-rocm: 83,362 compiler warnings, against 9 on linux-rocm` | the table below in this section (714,345 log lines against 2,455; the per-class counts; all of them in `ggml/src/ggml-cuda/*.cu`) | the cause is the language, not the arch count: HIP sources are compiled as plain C++ on Windows (`CXX_IS_HIPCC` on `WIN32`) and as HIP on Linux. Fix the warnings, or the C++-on-Windows choice? Their own `windows-rocm` job pays the identical cost |
 
-#### ~~A fifth: the ARM fp16 conversion~~ -- FILED as llama.cpp#30505
+#### ~~A fifth: the ARM fp16 conversion~~ -- MERGED as llama.cpp#30505 (b11761), patch 0019 dropped
 
-**Done 2026-10-14.** [ggml-org/llama.cpp#30505](https://github.com/ggml-org/llama.cpp/pull/30505),
-one commit, `+13/-3` in `ggml/src/ggml-cpu/simd-mappings.h`, open. Carried downstream meanwhile as
-`llama/patches/0019-ggml-cpu-select-the-neon-fp32-to-fp16-conversion.patch`; the full measurement
-record is [`docs/benchmarks/arm-fp16-conversion.md`](docs/benchmarks/arm-fp16-conversion.md).
+**Done 2026-10-14, merged 2026-10-20.** [ggml-org/llama.cpp#30505](https://github.com/ggml-org/llama.cpp/pull/30505),
+one commit, `+13/-3` in `ggml/src/ggml-cpu/simd-mappings.h`, merged as `119f68d7d` = **b11761** (the code lines
+as submitted, the comments shortened by the maintainers). Carried downstream until then as
+`llama/patches/0019-ggml-cpu-select-the-neon-fp32-to-fp16-conversion.patch`, dropped at the b11761 bump
+(`docs/history/dropped-llama-patches.md`); the full measurement record is
+[`docs/benchmarks/arm-fp16-conversion.md`](docs/benchmarks/arm-fp16-conversion.md).
 
 **Rebased the same day, at the maintainer's request, and the rebase changed the change.** `#30343`
 (`685f70894`) landed 26 minutes before that request and takes back half of `#30270`: F16C keeps
@@ -1288,6 +1403,19 @@ accepted upstream change deletes a patch from the bump checklist once the pin is
   own CPU picks. Worth an issue with the table from `docs/local-test-plan.md`; **no patch here** --
   overriding upstream's scoring downstream would be a maintenance burden with no end.
 
+- **A fourth, measured 2026-10-19 (F187): `--lora-scaled` / `--control-vector-scaled` cannot name an
+  absolute Windows path.** `common/arg.cpp` splits each `FNAME:SCALE` item with `string_split(item, ':')`
+  and demands exactly two parts, so `X:\models\a.gguf:1.0` is rejected as three. Measured against the
+  real parser in `BUGHUNT-MEASUREMENTS.md` M6 (of four argv forms only a relative path and plain
+  `--lora` were accepted). The obvious fix is to split on the **last** colon; small enough for a PR,
+  so draft it then. Downstream `ModelParameters` refuses such a path with the reason meanwhile.
+
+- **From the branch review of 2026-10-20 (F221): upstream's Anthropic answer names a matched stop
+  word in `stop_sequence` but says `stop_reason` `end_turn`**, where the Messages API specifies
+  `stop_reason` `stop_sequence`. The fix is one line in each of the two `to_json_anthropic*` functions
+  of `tools/server/server-task.cpp` plus an assertion in `test_compat_anthropic.py`; downstream
+  `AnthropicApiSupport` corrects it meanwhile. Small enough for a PR, so draft it then.
+
 (`0009`, `0010`, `0011` and `0013` are gone -- upstream fixed each defect itself -- and recorded in
 `docs/history/dropped-llama-patches.md`.)
 
@@ -1352,11 +1480,12 @@ These are JNI plumbing items for upstream API additions. Policy: add only after
   looks the binaries up on `PATH`, which a JVM process frequently does not have them on.
 
   What is still missing is the content part. Upstream's wire type is
-  `{"type":"input_video","input_video":{"data":""}}`, handled in
-  `oaicompat_chat_params_parse` and gated on `allow_video = mtmd_helper_support_video(mctx)`. Note it
-  calls `handle_media(..., accept_base64_uri = false)`, i.e. **raw base64 only** — unlike `image_url`,
-  it will not take a `data:` URI, so `ContentPart.videoFile(Path)` must emit the bare base64 payload,
-  not the `data:video/mp4;base64,...` form the image factories build.
+  `{"type":"input_video","input_video":{"data":""}}` (`video_url` with a `url` is accepted as
+  an alias), handled in `oaicompat_chat_params_parse` and gated on
+  `allow_video = mtmd_helper_support_video(mctx)`. Since b11803 it goes through the same
+  `handle_media(out_files, url, media_path)` as `image_url` (the `accept_base64_uri = false` of the
+  first version is gone), so raw base64, a `data:video/mp4;base64,...` URI and an HTTP(S) URL are all
+  taken; `ContentPart.videoFile(Path)` can emit the `data:` form the image factories build.
 
   (An earlier draft of this entry suggested smuggling video through
   `ContentPart.imageBytes(bytes, "video/mp4")`, on the reasoning that `mtmd_helper_bitmap_init_from_buf`
@@ -1384,7 +1513,7 @@ These are JNI plumbing items for upstream API additions. Policy: add only after
 
 - **Expose `--spec-draft-backend-sampling` toggle via `ModelParameters.setSpecDraftBackendSampling(boolean)`.** Added in b9437 (env `LLAMA_ARG_SPEC_DRAFT_BACKEND_SAMPLING`). Backend sampling for the speculative draft is enabled by default upstream but auto-disabled on `LLAMA_SPLIT_MODE_TENSOR` setups; an explicit Java-side setter lets callers force-disable it for benchmarking or for backends with sampler bugs. Speculative-decoding power users.
 
-- **Expose runtime reasoning control via `InferenceParameters.setReasoningControl(boolean)` + `LlamaModel.endReasoning(...)`.** Added in b9444–b9490: new `common_params_sampling::reasoning_control` flag arms the budget sampler so reasoning can be ended at runtime, and new `common_sampler_reasoning_budget_force(common_sampler *)` triggers the end-of-thinking token injection on the next sample. Upstream also adds a `POST /v1/chat/completions/control` server endpoint accepting `{"id": "...", "action": "reasoning_end"}`. Java mapping would be: (a) `InferenceParameters.setReasoningControl(boolean)` arms the sampler on the inference run, (b) a new `LlamaModel.endReasoning(int slotId)` (or per-streaming-task-id) JNI method calls the upstream `common_sampler_reasoning_budget_force` against the slot's sampler. Useful for interactive UIs that want a "skip thinking and answer now" button. Relevant only for reasoning-trained models (DeepSeek-R1, Qwen3-Thinking, GPT-OSS-Reasoner, etc.).
+- **Expose runtime reasoning control via `InferenceParameters.setReasoningControl(boolean)` + `LlamaModel.endReasoning(...)`.** (F252, 2026-10-21, added the budget's tag fields to the builder but deliberately not `reasoning_control`: the flag only arms the sampler for the `reasoning_end` action below, so a setter without that action would do nothing. Both land together or not at all.) Added in b9444–b9490: new `common_params_sampling::reasoning_control` flag arms the budget sampler so reasoning can be ended at runtime, and new `common_sampler_reasoning_budget_force(common_sampler *)` triggers the end-of-thinking token injection on the next sample. Upstream also adds a `POST /v1/chat/completions/control` server endpoint accepting `{"id": "...", "action": "reasoning_end"}`. Java mapping would be: (a) `InferenceParameters.setReasoningControl(boolean)` arms the sampler on the inference run, (b) a new `LlamaModel.endReasoning(int slotId)` (or per-streaming-task-id) JNI method calls the upstream `common_sampler_reasoning_budget_force` against the slot's sampler. Useful for interactive UIs that want a "skip thinking and answer now" button. Relevant only for reasoning-trained models (DeepSeek-R1, Qwen3-Thinking, GPT-OSS-Reasoner, etc.).
 
 - **Expose `llama_context_params::n_outputs_max` via `ModelParameters.setMaxOutputs(int)`.** Added in b9444–b9490 (default `-1` = derived from `n_batch`). Caps the number of output slots allocated per context; relevant for memory-constrained setups that always run with `logits_all=false` and want to prevent over-allocation when `n_batch` is large. Trivial JNI plumbing (one `cparams` field passthrough); add when a user reports OOM on context creation tied to output slot pre-allocation.
 
@@ -1492,7 +1621,7 @@ flags, the `acquire`/`release` context-guard unit tests, the `OSInfo` alias test
   matters. Recorded with the reasoning so this is not reopened as an oversight.
 
 - **`ContentPart.videoFile(...)` — see the video-input entry above** for the wire shape upstream
-  expects (`input_video`, raw base64, not a `data:` URI).
+  expects (`input_video`; raw base64, a `data:` URI or an HTTP(S) URL, as for images since b11803).
 
 ## Open — cross-cutting (slice for this repo)
 
@@ -1551,32 +1680,11 @@ SME**, which Graviton 4 lacks and no current instance provides. EC2 Mac also bil
 minimum** (dedicated host), so it is the expensive way to answer nothing.
 
 
-## Handed over from the modular-natives session (2026-10-11) -- read the first item before merging #507
+## Handed over from the modular-natives session (2026-10-11)
 
 Appended on `local/cpu-module-isolation-test` by the session that shipped the modular natives
-(#504 to #506) and watched their two full runs (38065017595, 38069595101). Nothing above was moved.
-Items `main` already carries through #506 are named, not repeated.
-
-### Before #507 merges: two `0018` patches for the same hunk
-
-`main` (cfc10fd2, #506) carries `llama/patches/0018-ggml-amx-free-the-aligned-buffer.patch`; this
-branch carries `llama/patches/0018-ggml-amx-aligned-free.patch`. Both replace the same line of
-`ggml/src/ggml-cpu/amx/amx.cpp` (main's adds a four-line comment above the call, this one is bare).
-After a merge both files exist, the applier takes them in filename order, the first applies, the
-second no longer finds its `-` line and aborts the configure with "does not apply cleanly" -- every
-native build job red, on every platform. `git merge-tree main HEAD` (2026-10-11) reports a textual
-conflict in `TODO.md` **only**; the two patch files, CLAUDE.md (two `0018` rows in the patch table)
-and CHANGELOG.md (two "Fixed" bullets for the same defect) merge *silently*. Resolve in the merge
-commit: keep exactly one patch file (the comment is the only difference), one patch-table row and
-one CHANGELOG bullet, and keep this branch's `llama/cmake/check-aligned-free-pairing.cmake` guard
-either way (main has none). For the `TODO.md` conflict: take main's wording for the three places
-#506 touched -- a fifth "Modular natives -- follow-ups" item (the Windows ROCm job is the pipeline's
-critical path at 4:15 h and builds uncached), the corrected sccache sentence under "Upstream
-submissions" (the `-Xclang` that sccache 0.18.0 refuses comes from CMake's
-`Platform/Windows-Clang.cmake` runtime-library selection on every plain-clang Windows TU, not only
-from llama.cpp's `-fno-pch-timestamp`; CLAUDE.md "sccache on every Windows Ninja job but one" on
-main has the measurement) and the AMX bullet under "Needs a machine" -- and this branch's structure
-for everything else.
+(#504 to #506) and watched their two full runs (38065017595, 38069595101); merged as #507 with one
+`0018` patch and the `check-aligned-free-pairing.cmake` guard. What is still open from it:
 
 ### Open from that session, recorded nowhere else
 
@@ -1619,33 +1727,28 @@ for everything else.
   every probe "failed" until the fixtures were given a `#!/bin/sh` line. The first run of that probe
   reported all five layouts broken, including the one that demonstrably works in CI -- the harness,
   not the logic.
-- **Everything behind `package` has never run on the modular natives.** Run 38069595101 skipped
-  `package` (it `needs` the Windows clang test job, red with the AMX crash); this branch's run
-  38112603436 reaches it and fails on the Windows OpenVINO module jar (the blocker at the top of
-  this file). Still unexercised after that: the smoke-set assembly (`package-smoke-sets.sh`),
-  `smoke-natives-jars.sh` (classes + all 23 natives jars on the classpath **and** the module path),
-  the bytecode gate and `verify-native-deps.py` over all 23 jars, the four `smoke-natives` rows
-  (loader line naming every module jar of the platform, the HTTPS round, the RPC smoke, the
-  bytecode gate over the set), `smoke-natives-macos`, `smoke-agent-linux`, and the publish jobs
-  attaching the Maven artifacts. Expect further rounds there; the `windows-x86-64` smoke row runs
-  on the same CPU lottery as the Windows test job, so it exercises AMX only when GitHub hands it an
-  Intel runner.
-- **The two server defaults disagree, by design or not -- owner's call.** Upstream #30159 (b11529)
-  moved llama-server's default port from 8080 to 9931, which `NativeServer.DEFAULT_PORT` follows
-  because it inherits the forwarded argv; `OpenAiServerConfig.DEFAULT_PORT` stayed 8080 as the
-  project's own constant. Raised 2026-10-09, never answered: align `OpenAiCompatServer` to 9931
-  (one constant, its tests, README) or keep the split and say so in the README.
 - **Maven plugins without a pinned version, and no `requirePluginVersions` enforcer rule** (all
   four repos; raised 2026-10-01, never answered). `clean`, `resources`, `install`, `deploy`, `site`
   and in places `compiler`/`jar` run at whatever version the Maven on the runner defaults to, which
   differs between CI and a local build (the parent's `` here covers the plugins
   more than one module uses, not these). `check-versions.py` would then also compare them across
   the siblings. One pull request per repo, warnings-only until all four have landed.
-- **`SECURITY.md` and `CONTRIBUTING.md` are the two per-repo files never compared.** Every other
-  file that was "probably the same" across the four repos is either in `shared-files.sha256`
-  byte-for-byte or as a `?repo` entry; these two were left out on 2026-10-01 as "differs per repo,
-  probably wanted, not checked". Diff them once and either add `?repo` entries or write down why
-  they differ.
+- ~~**`SECURITY.md` and `CONTRIBUTING.md` are the two per-repo files never compared.**~~ **Compared
+  on 2026-10-21 (with the repository name replaced by `{repo}`, as a `?repo` entry hashes): no two
+  of the four are equal, so no `?repo` entry is possible, and they differ for content, not for
+  wording alone.** `SECURITY.md`: the supported-versions table and the scope are per repository by
+  nature (`5.x` and the JNI layer here, `1.x` and the OpenCL kernels in BitcoinAddressFinder, the
+  `1.2.x` reactor line in srcmorph, one class in streambuffer); the shared part -- the private
+  advisory form, the mail address, 14 days to acknowledge, 30 to a remediation timeline, a 90-day
+  embargo -- says the same in four wordings. `CONTRIBUTING.md`: build commands, prerequisites and test
+  policy are per repository (CMake and natives here, the LMDB switch in BitcoinAddressFinder, PIT and
+  jcstress in streambuffer, the reactor in srcmorph); the PR workflow, the commit convention and the
+  conduct and licence paragraphs are shared in substance, again in different words. Making the shared
+  part byte-identical would need a split into a shared file and a per-repository one, which GitHub
+  does not read (it shows one `SECURITY.md` / `CONTRIBUTING.md`); a shared-files entry is therefore
+  not proposed. The comparison found this repository's copies stale and they were corrected
+  (Java 8 floor, JUnit Jupiter, the `llama/` paths, the hand-written `jllama.h`); the `5.x` row of
+  `SECURITY.md` changes with the release (release item 3 above).
 - **SonarCloud reported "1 new issue" on #486 and nobody looked.** The sandbox's proxy answers 403
   for sonarcloud.io, so the finding was never read; the quality gate has passed since, so it is
   low, but it is unread.
diff --git a/docs/benchmarks/arm-fp16-conversion.md b/docs/benchmarks/arm-fp16-conversion.md
index 714015c8d..a054640e3 100644
--- a/docs/benchmarks/arm-fp16-conversion.md
+++ b/docs/benchmarks/arm-fp16-conversion.md
@@ -10,14 +10,17 @@ Measured **2026-10-13**. One `#define` is missing from the NEON branch of
 `ggml/src/ggml-cpu/simd-mappings.h`, so `GGML_CPU_FP32_TO_FP16` falls through to a software bit
 manipulation while the branch's own single-instruction conversion sits there unused. Same shape of
 defect as the one [ggml-org/llama.cpp#30270](https://github.com/ggml-org/llama.cpp/pull/30270) fixed
-for x86 F16C on 2026-10-11 -- in the other direction, on a different architecture, still open.
+for x86 F16C on 2026-10-11 -- in the other direction, on a different architecture. **Merged upstream on
+2026-10-20 as [#30505](https://github.com/ggml-org/llama.cpp/pull/30505), first tagged at b11761**; the rest of
+this page is the record as it was written while the defect was open.
 
 Found while isolating that PR ([`upstream-f16c-30270.md`](upstream-f16c-30270.md)): a `#error` guard
 written to stop the omission recurring fired immediately on upstream's unmodified header.
 
-**Submitted upstream as [ggml-org/llama.cpp#30505](https://github.com/ggml-org/llama.cpp/pull/30505)**
-and carried here until it merges as
-[`llama/patches/0019-ggml-cpu-select-the-neon-fp32-to-fp16-conversion.patch`](../../llama/patches/0019-ggml-cpu-select-the-neon-fp32-to-fp16-conversion.patch).
+**Submitted upstream as [ggml-org/llama.cpp#30505](https://github.com/ggml-org/llama.cpp/pull/30505)**, carried
+here as `llama/patches/0019-ggml-cpu-select-the-neon-fp32-to-fp16-conversion.patch` until it merged (b11761; the
+patch was dropped at that bump, see [`docs/history/dropped-llama-patches.md`](../history/dropped-llama-patches.md)).
+The merged commit (`119f68d7d`) carries the same code lines; the maintainers shortened the comments.
 
 > **Read "One day later" before taking the guard or the F16C move in this page at face value.** The
 > PR was rebased on 2026-10-14 after upstream #30343 reverted half of #30270; the guard grew a
@@ -53,7 +56,7 @@ Read at b11576, all four branches:
 | `__riscv && __riscv_zfhmin` | yes | yes | yes |
 
 Two of four were incomplete. #30270 completed one of them -- from 88 lines below, outside the chain.
-The ARM one is open, in the direction that fix did not cover.
+The ARM one was open, in the direction that fix did not cover, until #30505 (b11761).
 
 **The sharpest way to state it:** the fallback reads
 `#define GGML_CPU_FP32_TO_FP16(x) GGML_COMPUTE_FP32_TO_FP16(x)`, and `GGML_COMPUTE_FP32_TO_FP16` is
diff --git a/docs/c-api.md b/docs/c-api.md
index ae8165400..9bb1d8803 100644
--- a/docs/c-api.md
+++ b/docs/c-api.md
@@ -34,19 +34,20 @@ Every operation of `LlamaModel` and `LlamaQuantizer` is in it (the roadmap's "B.
 | `jllama_build_info()` | the llama.cpp build inside, `b-` |
 | `jllama_last_error()` | the message of the last failed call on this thread (thread-local) |
 | `jllama_string_free()`, `jllama_floats_free()`, `jllama_tokens_free()` | release what the library returned through an out-parameter |
-| `jllama_model_load()` / `jllama_model_free()` | a model from llama-server's command line (`-m`, `-c`, `-ngl`, ... and `--vocab-only`), with an optional load-progress callback |
-| `jllama_model_meta()` | the model's metadata object (vocab, context, modalities, chat template, the GGUF key/value map) |
+| `jllama_model_load()` / `jllama_model_free()` | a model from llama-server's command line (`-m`, `-c`, `-ngl`, ... and `--vocab-only`, which loads the tokenizer only: such a model answers the meta, tokenize and detokenize calls and refuses every other with `JLLAMA_STATUS_ERROR`), with an optional load-progress callback |
+| `jllama_model_meta()` | the model's metadata object (vocab, context, modalities, the `media_marker` a raw multimodal prompt places, chat template, the GGUF key/value map) |
 | `jllama_chat_completions()` | one OpenAI chat completion, blocking |
 | `jllama_chat_completions_stream_request()` / `_receive()` / `jllama_task_release()` | a streamed one: a task id, then one `{"data": , "stop": }` envelope per call until `stop` |
 | `jllama_chat_completions_request()` | a streamed chat completion whose results are llama.cpp's raw result objects (received with `jllama_completions_stream_receive`) |
-| `jllama_completions()`, `jllama_completions_oai()`, `jllama_infill()` | a raw `/completion`, an OpenAI `/v1/completions`, a fill-in-the-middle request, blocking |
-| `jllama_completions_stream_request()` / `jllama_completions_stream_receive()` | a raw streamed completion (an infill one when the body has the infill fields): each result object with a `"stop"` key |
+| `jllama_completions()`, `jllama_completions_oai()`, `jllama_infill()` | a raw `/completion` (an infill one when the body has the infill fields, the chat template rendered into the prompt when it has `messages`, with the template's own stop strings and, for a template with thinking tags, `reasoning_budget_tokens` kept to those tags as on the chat paths, the tokens the template preserves rendered as text beside the request's own `preserved_tokens`; without `messages` the request names the budget's tags itself, and a missing `reasoning_budget_message` is the server's default as on the chat paths, so the first end tag is forced; a chat field it cannot apply beside them -- `response_format`, `tools`, `tool_choice`, `parallel_tool_calls`, `top_logprobs`, `thinking_budget_tokens` -- is `JLLAMA_STATUS_INVALID_REQUEST`), an OpenAI `/v1/completions`, a fill-in-the-middle request, blocking |
+| `jllama_completions_oai_stream_request()` | a streamed OpenAI `/v1/completions` (the body as for `jllama_completions_oai()`), received with `jllama_chat_completions_stream_receive()`: `text_completion` chunks, the last with `usage` |
+| `jllama_completions_stream_request()` / `jllama_completions_stream_receive()` | a raw streamed completion (infill fields and `messages` as for `jllama_completions()`): each result object with a `"stop"` key |
 | `jllama_apply_template()` | the chat template applied to a messages body: the prompt text |
 | `jllama_embed()`, `jllama_embeddings()` | one embedding vector; an `/embeddings` or `/v1/embeddings` request (`--embedding`) |
 | `jllama_rerank()` | documents scored against a query (`--reranking`) |
 | `jllama_tokenize()`, `jllama_tokenize_json()`, `jllama_detokenize()`, `jllama_detokenize_json()` | ids or the `/tokenize` response (with pieces on request); bytes with their length, or the `/detokenize` response |
 | `jllama_metrics()` | the counters plus the `slots` array (`LlamaModel.getMetrics()`) |
-| `jllama_slot_action()` | save, restore or erase a slot's KV cache |
+| `jllama_slot_action()` | save, restore or erase a slot's KV cache; the file of a save or restore is a file name inside the `--slot-save-path` directory, as upstream's `/slots` route takes it (a name with a directory part or `..` is `JLLAMA_STATUS_INVALID_REQUEST`, a model without the directory `JLLAMA_STATUS_ERROR`) |
 | `jllama_lora_adapters()`, `jllama_set_lora_adapters()` | the LoRA list; the scales from `[{"id", "scale"}]` |
 | `jllama_configure()` | `n_threads`, `n_threads_batch`, `slot_prompt_similarity` at run time |
 | `jllama_systemone()` | a `/v1/systemone` request to a decision model |
@@ -77,6 +78,12 @@ And the `jllama_api.h` header ships in every natives directory, so a consumer of
   or out-parameter), `INTERRUPTED` (the model was freed under the call). On anything but `OK`,
   `jllama_last_error()` has the message. The Java side maps `INVALID_REQUEST` to
   `InvalidRequestException` and the rest to `LlamaException`, the same rule as before.
+- **One request, one answer.** A completion or chat request takes one prompt and asks for one
+  choice: several prompts in one request (an array of strings or of token arrays) and `"n"` greater
+  than 1 are `INVALID_REQUEST` on the blocking, raw and streamed paths. llama.cpp's HTTP server
+  answers them with several choices from child tasks, which this engine does not create; answering
+  with one would drop the rest silently. Send separate requests (several slots, `-np`, run them at
+  once).
 - **Logging.** `jllama_set_log_callback(cb, user_data)` is process-wide and routes *every* line
   (llama.cpp, ggml, the server's `srv`/`slot` lines) to `cb` instead of the console, on the logger's
   worker thread. `jllama_set_log_callback(NULL, NULL)` drains the lines queued for the previous
diff --git a/docs/feature-investigation-ide-agent-backend.md b/docs/feature-investigation-ide-agent-backend.md
index af12ba582..4333d472b 100644
--- a/docs/feature-investigation-ide-agent-backend.md
+++ b/docs/feature-investigation-ide-agent-backend.md
@@ -73,7 +73,7 @@ SPDX-License-Identifier: MIT
 "github.copilot.chat.customOAIModels": {
   "my-model": {
     "name": "My Model",
-    "url": "http://127.0.0.1:8080/v1/chat/completions",
+    "url": "http://127.0.0.1:9931/v1/chat/completions",
     "toolCalling": true, "vision": false, "thinking": false,
     "maxInputTokens": 128000, "maxOutputTokens": 16000,
     "requiresAPIKey": false
@@ -84,7 +84,7 @@ SPDX-License-Identifier: MIT
 
 Copilot reads capability flags `toolCalling`, `vision`, `thinking`, `maxInputTokens`/`maxOutputTokens`. It sends standard OpenAI chat bodies with `messages`, `tools`, `tool_choice`, `stream:true`. A documented crash — microsoft/vscode issue #273482 ("OpenAI Compatible models return TypeError: Cannot read properties of undefined (reading 'cached_tokens')"), shows `TypeError: Cannot read properties of undefined (reading 'cached_tokens') at SX.push (…github.copilot-chat-0.33.2025102701…)` reproduced with LM Studio models in agent and ask mode — occurs when the streamed `usage` lacks `prompt_tokens_details.cached_tokens`. [GitHub](https://github.com/microsoft/vscode/issues/273482)
 
-**Continue.dev** (`config.yaml`, schema v1): `provider: openai` + `apiBase: http://127.0.0.1:8080/v1` + `model` + `roles: [chat, edit, apply]`. For OpenAI-compatible non-chat completion: `useLegacyCompletionsEndpoint: true` forces `/v1/completions`. [Continue](https://docs.continue.dev/customize/model-providers/top-level/openai) Continue's native `llama.cpp` provider posts to `/completion` (singular), not `/completions` (issue #4991). `requestOptions.headers` carries auth; `capabilities: [tool_use, image_input]` can be declared.
+**Continue.dev** (`config.yaml`, schema v1): `provider: openai` + `apiBase: http://127.0.0.1:9931/v1` + `model` + `roles: [chat, edit, apply]`. For OpenAI-compatible non-chat completion: `useLegacyCompletionsEndpoint: true` forces `/v1/completions`. [Continue](https://docs.continue.dev/customize/model-providers/top-level/openai) Continue's native `llama.cpp` provider posts to `/completion` (singular), not `/completions` (issue #4991). `requestOptions.headers` carries auth; `capabilities: [tool_use, image_input]` can be declared.
 
 **Cline / Roo Code:** Settings → "OpenAI Compatible" → Base URL (must include `/v1`), API key, model ID. Roo internally uses Anthropic message format then transforms to OpenAI `ChatCompletionTool`; it accumulates streamed fragments by `index`; finalizes on `finish_reason:"tool_calls"`. `parallelToolCalls:true` is the default.
 
@@ -178,12 +178,12 @@ OpenAI shape required: `tools:[{type:"function",function:{name,description,param
 
 ## Exact config snippets
 
-**GitHub Copilot (VS Code ≥1.122) — Custom Endpoint (preferred):** *Chat: Manage Language Models* → Add Models → **Custom Endpoint** → display name, API key (any string if none), Base URL `http://127.0.0.1:8080/v1`, model ID, apiType `chat-completions`. For older stable builds, use the legacy object in `settings.json`:
+**GitHub Copilot (VS Code ≥1.122) — Custom Endpoint (preferred):** *Chat: Manage Language Models* → Add Models → **Custom Endpoint** → display name, API key (any string if none), Base URL `http://127.0.0.1:9931/v1`, model ID, apiType `chat-completions`. For older stable builds, use the legacy object in `settings.json`:
 ```json
 "github.copilot.chat.customOAIModels": {
   "local-qwen": {
     "name": "Local Qwen (llama.cpp)",
-    "url": "http://127.0.0.1:8080/v1/chat/completions",
+    "url": "http://127.0.0.1:9931/v1/chat/completions",
     "toolCalling": true,
     "vision": false,
     "thinking": false,
@@ -204,7 +204,7 @@ models:
   - name: local-fim
     provider: llama.cpp
     model: your-model.gguf
-    apiBase: http://127.0.0.1:8080
+    apiBase: http://127.0.0.1:9931
     roles: [autocomplete]
     autocompleteOptions:
       debounceDelay: 250
@@ -212,21 +212,21 @@ models:
   - name: local-chat
     provider: openai
     model: your-model
-    apiBase: http://127.0.0.1:8080/v1
+    apiBase: http://127.0.0.1:9931/v1
     apiKey: sk-local
     roles: [chat, edit, apply]
 ```
 
 **Cline / Roo Code (VS Code settings UI):**
 - API Provider: **OpenAI Compatible**
-- Base URL: `http://127.0.0.1:8080/v1`
+- Base URL: `http://127.0.0.1:9931/v1`
 - API Key: `sk-local` (any string if no auth)
 - Model ID: `your-model`
 - (Roo Code ≥3.37: model must support native tool calling, or roll back to 3.36.16 for the XML option.)
 
 **Twinny (Providers → Code/FIM provider):**
 - Provider: `llamacpp`
-- Hostname/port: `127.0.0.1` / `8080`
+- Hostname/port: `127.0.0.1` / `9931`
 - FIM endpoint path: `/infill`
 - FIM Template: match the model (e.g. Qwen2.5-Coder / DeepSeek / CodeLlama)
 
@@ -234,12 +234,12 @@ models:
 ```toml
 [model.completion.http]
 kind = "llama.cpp/completion"
-api_endpoint = "http://127.0.0.1:8080"   # no /v1
+api_endpoint = "http://127.0.0.1:9931"   # no /v1
 prompt_template = "<|fim_prefix|>{prefix}<|fim_suffix|>{suffix}<|fim_middle|>"
 
 [model.chat.http]
 kind = "openai/chat"
-api_endpoint = "http://127.0.0.1:8080/v1"
+api_endpoint = "http://127.0.0.1:9931/v1"
 ```
 
 ## Caveats / open risks (2026 moving targets)
diff --git a/docs/feature-investigation-llama-stack-client-kotlin.md b/docs/feature-investigation-llama-stack-client-kotlin.md
index 11c7bdcc4..dc0bcf325 100644
--- a/docs/feature-investigation-llama-stack-client-kotlin.md
+++ b/docs/feature-investigation-llama-stack-client-kotlin.md
@@ -189,9 +189,13 @@ RxJava, Kotlin coroutines from Java consumers.
 
 **Status: SHIPPED** (PR #188, commit `de457b2`).
 `LlamaModel.completeBatch(List)`,
-`completeBatchWithStats(...)`, and `chatBatch(List)` dispatch
-all requests at once via the existing async wrappers; results returned in
-input order. Throughput scales with `ModelParameters.setParallel(N)`
+`completeBatchWithStats(...)`, and `chatBatch(List)` return the
+results in input order. `completeBatch` and `completeBatchWithStats` submit
+every request on the calling thread, then receive the results in that order;
+`chatBatch` runs each request on a per-call daemon pool of at most 64 threads
+(`BatchDispatch.MAX_CHAT_THREADS`), the rest starting as threads free up.
+(Both replaced the original dispatch through the async wrappers, ledger
+F167/F200.) Throughput scales with `ModelParameters.setParallel(N)`
 (default `N=1` runs sequentially across the single slot).
 
 **Gap.** llama.cpp natively serves parallel slots; the compiled-in server
diff --git a/docs/history/dropped-llama-patches.md b/docs/history/dropped-llama-patches.md
index 1107416af..a25208755 100644
--- a/docs/history/dropped-llama-patches.md
+++ b/docs/history/dropped-llama-patches.md
@@ -11,6 +11,65 @@ themselves. Moved here from `CLAUDE.md`, which lists only the patches still carr
 records why the patch went and what guard, if any, stays behind -- read the matching note before
 reintroducing a patch for the same defect.
 
+**`0019` was dropped at the b11761 bump** (the first number that carries the fix is b11761). Upstream merged
+this project's own [ggml-org/llama.cpp#30505](https://github.com/ggml-org/llama.cpp/pull/30505) (commit
+[`119f68d7d`](https://github.com/ggml-org/llama.cpp/commit/119f68d7d441d66bdf8ebd117ca0deee2119fbcc),
+"ggml-cpu : select the NEON fp32->fp16 conversion", 2026-10-20, first tagged at **b11761**) -- the change the
+patch carried: the `__ARM_NEON` branch of `ggml/src/ggml-cpu/simd-mappings.h` selects `GGML_CPU_FP32_TO_FP16` as
+`GGML_CPU_COMPUTE_FP32_TO_FP16` (one `fcvt` instead of the portable bit manipulation: 2.45x-13.3x on five ARM
+core types, bit-exact over all 2^32 inputs, [`docs/benchmarks/arm-fp16-conversion.md`](../benchmarks/arm-fp16-conversion.md)),
+plus the `#error` pair that refuses a branch defining a `GGML_CPU_COMPUTE_*` macro without selecting it, with the
+`&& !defined(__F16C__)` carve-out for the exception #30343 documents. The merged commit carries the submitted code
+lines; the maintainers shortened the three comments, which is why the drop-check, and not a byte comparison, was
+the right test. **Here the applier fired**: the patch's `+` lines are in the tree, so its one hunk "does not
+apply" at b11761 -- the drop-check's one printed line (`git show b11761:ggml/src/ggml-cpu/simd-mappings.h | sed -n
+'/^#if defined(__ARM_NEON)/,/^#elif/p' | grep -n GGML_CPU_FP32_TO_FP16` prints line 37; at b11760 it printed
+nothing). Dropped, not refreshed, as its row said.
+
+**No guard stayed behind, and none was needed.** The patch added no callable: it changed a preprocessor
+selection, and the `#error` guard it introduced is upstream's own now, so a branch that defines a COMPUTE macro
+without selecting it fails to compile in upstream's tree. What stays is the measurement record (the benchmark
+page, with the merge noted at its head) and the TODO entry; the `0019` row of `CLAUDE.md`'s patch table went.
+
+**`0016` was dropped at the b11760 bump** (the first number that carries the architecture is b11709). Upstream
+merged [ggml-org/llama.cpp#30673](https://github.com/ggml-org/llama.cpp/pull/30673) (commit
+[`62318cd57`](https://github.com/ggml-org/llama.cpp/commit/62318cd57dc63900ecb153272c0b10f78550855d),
+"model : add Kolibri-1 (Aleph-Alpha) support", 2026-10-17, first tagged at **b11709**; b11765, #30865, then
+dropped two redundant precision overrides from its router): `src/models/kolibri1.cpp`, the `kolibri1` arch, its
+NEOX rope entry, a converter (`conversion/kolibri1.py` over `AfmoeModel`, registering the 128k vocabulary as the
+`qwen2` pre-tokenizer by hash) and a `test-llama-archs` row. It is the architecture the patch had: GQA with
+QK-RMSNorm, sliding-window layers with NEOX RoPE between full-attention layers without positional encoding,
+sandwich norms, one ungated shared expert, and the router that selects on `logits + expert_bias` and weights by
+the unbiased sigmoid (`ggml_argsort_top_k` over the biased logits, handed to `build_moe_ffn` as the selection).
+**Here the applier fired on a collision, not on shifted context:** the patch creates `src/models/kolibri1.cpp`,
+and `git apply` refuses a new-file hunk for a path that exists ("already exists in working directory") -- a patch
+that cannot be refreshed, only dropped. Upstream is not a copy of the patch; it was written from the same
+reference and the same community ports.
+
+**What upstream does not load, recorded so the decision is visible:** its loader reads `expert_gating_func`,
+`expert_weights_norm`, `expert_shared_count`, `expert_shared_feed_forward_length` and
+`attention.sliding_window_pattern` as **required** keys, requires `output.weight`, and knows no pre-tokenizer
+`kolibri1` (`llama-vocab.cpp` throws `unknown pre-tokenizer type`). The patch took all of that loosely because the
+two community converters disagreed -- gating 2 (AFMoE-based, Eliasfpv28) against 5 (Qwen3-MoE-based,
+Hob-forge / mjwsolo/localcode#101), `qwen2` against `kolibri1`, an explicit against a tied output, the norm key
+present or absent. So the GGUFs of the Qwen3-MoE-based port **do not load** from b11709 on, and those of the
+AFMoE-based port load only if they carry every required key, which this sandbox cannot check (no model
+download). **The answer is reconversion** with upstream's `convert_hf_to_gguf.py` at b11709 or later, not a
+compatibility patch: `0016` was a temporary carry until upstream registered the architecture (its own row said
+so), a shim for pre-release community metadata would be a patch forever, and upstream's converter is the format
+of record now. README ("Kolibri-1") says so to consumers.
+
+**The runnable guard stays, re-pointed at upstream's code:** `test_kolibri1.cpp` keeps the numerical comparison
+against Aleph Alpha's reference -- one batch and token by token through the iSWA cache, in upstream's converter
+format, with `norm_topk_prob` off and on -- and the DeepSeek-router control. The two format rows of the dropped
+dialects went: `Qwen3MoeDialectWithTiedOutputMatchesTheReference` (gating 5, pre `kolibri1`, tied output: three
+things upstream refuses) and `AnotherGatingFunctionIsRejected`, which assumed a rejection upstream does not make
+(it passes an unknown value on to `build_moe_ffn`, where gating 1 is softmax). 5 -> 3 tests. Verified against
+upstream's implementation at b11799, the end of this bump series: the three remaining tests green on the first
+run, no tolerance changed. `REUSE.toml`'s `MIT AND Apache-2.0` annotation for the patch file went with it
+(Eliasfpv28's Apache-2.0 additions are no longer in the tree). **GPU backends remain unverified for the model**,
+as they were with the patch.
+
 **`0012` was dropped at the b11632 bump** (the first number that carries the fix is b11626). Upstream merged
 [ggml-org/llama.cpp#29677](https://github.com/ggml-org/llama.cpp/pull/29677) (commit
 [`df18bee11`](https://github.com/ggml-org/llama.cpp/commit/df18bee11e96557b07aa7b12db0976f3c7ac9165),
diff --git a/docs/history/llama-cpp-breaking-changes.md b/docs/history/llama-cpp-breaking-changes.md
index 7fdd3044e..341eaec53 100644
--- a/docs/history/llama-cpp-breaking-changes.md
+++ b/docs/history/llama-cpp-breaking-changes.md
@@ -895,3 +895,19 @@ Used during `llama.cpp` version bumps: when upgrading, scan this file from the r
 | b11632–b11639 | 7 commits on master's head of 2026-10-14 (`a0e6dc738` = b11639, pinned through `JLLAMA_GIT_COMMIT` ahead of the tag -- upstream's newest tag is b11627), 21 files, ~691/90 lines (57 KiB; no `tools/ui`, no `.github/workflows`, no `common/`, `include/`, `src/`, `tools/`, root `CMakeLists.txt` unchanged, so the `_DEFAULT` re-check of "Modular natives" has nothing new). `ggml/src/ggml-cpu/ggml-cpu.c` (#30454, b11633): `GGML_OP_SET_ROWS` moves to the `n_tasks = n_threads` group -- every thread ran it with its own row of the F16 -> F32 conversion buffer while the planner sized the work buffer for one, so threads > 0 wrote past it whenever the graph held another multi-threaded op; every CPU module rebuilds it. `ggml/src/ggml-cpu/simd-mappings.h` (#30343, b11635): x86 with F16C keeps the lookup table for FP16 -> FP32 (clang lowers the F16C intrinsic into the dot products' live accumulator and serialises the loop, 45-65 % slower Q8_0/Q4_0 `MUL_MAT_ID`; the Windows x86-64 jar is built with plain clang) -- the carve-out of `0019`, see the patches row. `ggml-sycl` (#29107, b11636): persistent IQ3_S/IQ3_XXS reorder **and a build change: `GGML_SYCL_DNN` (default ON) now fails the configure when oneDNN is missing or CPU-only** instead of building without it -- `build-linux-x86_64-sycl` installed only DPC++ + MKL (the shipped `libggml-sycl.so` of the b11558 snapshot has no `libdnnl` in its `DT_NEEDED`), so the job now also installs `intel-oneapi-dnnl-devel`; the Windows job already installed `intel.oneapi.win.dnnl` and its module imports `dnnl.dll`. `ggml-opencl` (#30186, b11639): a strided CLAMP kernel for non-contiguous views (the OpenCL module jars). Docs, `gguf-py` and `.devops` Dockerfiles otherwise. **No project source change; one workflow change (the oneDNN package).** |
 | b11632–b11639 | patches + upstream verification | **All twelve patches apply unchanged** (`git apply` in order on a pristine `a0e6dc738` worktree; `check-patches.py`: 12 patches, 94 hunks, 0 problems). **`0019`'s `&& !defined(__F16C__)` carve-out is load-bearing from this pin on**, and holds: `gcc -E` of the patched `simd-mappings.h` is silent for `-march=x86-64`, `-msse4.2`, `-march=haswell`, `-march=sapphirerapids`, `-march=znver4` and `-mavx -mf16c`. Drop-checks against pristine b11639 are those of the b11632 row, the range touching no other patch-target file: no `kolibri` (`0016`), no `common_log` hook (`0014`), no `stop_server` (`0015`), uncast `_mm_prefetch` (`0017`), `free(buffer->context)` (`0018`), the software fp32 -> fp16 conversion on `__ARM_NEON` (`0019`, #30505 still open), the `GetCommandLineW` override and no `common_params_parse_main` (`0001`), the unconditional load-progress assignment (`0002`), no `LLAMA_SERVER_WORKER_CMD` (`0008`), no `get_slot_prompt_similarity` (`0003`). Through the production path -- `rm -rf build`, `FetchContent` by commit, the applier, the pin check reporting `fetched commit count 11639 = b11639` -- the static test build, `ctest` **601/601**, and the model-free native Java tests over the library (`NativeLibraryLoadSmokeTest` with the `LlamaCppVersion` pin `b11639` against the linked build, `LlamaLoggerTest`, `RpcServerTest`, `BackendLoadTest`: 27/27). `check-natives.py`: 23 natives jars, 0 disagreements. Not run locally: the model-backed Java suite, the modular build and the SYCL job with oneDNN -- CI covers them. |
 | b11639 (follow-up) | `patches/0020`, a runtime regression from b11574..b11612 | **An `--embedding` context that is asked to generate crashed the process** -- found only now because no Publish run reached the Java tests between b11558 and the b11639 dispatch (38648257387): `ErrorHandlingTest` (model with `enableEmbedding()`, first test an infill request) died with SIGSEGV in `ggml_gallocr_alloc_graph` on Linux and 0xC0000005 under both Windows toolchains. Cause: #30445 (`cb55ffe6e`, "graph: skip the LM head for pooled embeddings") moved the LM head's expansion into `build_logits()` after `build_pooling()`, so the reserved embeddings graph and a generation graph on the same context have the same node and leaf counts but the leafs in another order, and `ggml_gallocr_needs_realloc()` validates nodes and their sources but of the leafs only the count -- an input leaf (`out_ids`) received the slot of a weight and `ggml_vbuffer_tensor_alloc()` indexed `galloc->buffers[-1]`. Reproduced locally with a random 2-layer llama GGUF (FIM tokens set), confirmed by reverting `cb55ffe6e`, fixed by `0020` (validate the leafs like the nodes), greedy output identical with and without `--embedding`; guard `test_gallocr_leafs.cpp`. Upstream master (`a0e6dc738`) unchanged. |
+| b11639–b11760 | upstream verification (121 commits, `b68449857` = b11760, 2026-10-14 .. 2026-10-20) | **One link requirement, no JNI-level break.** `tools/server/server-connect.cpp` (b11744, #28277, `--connect`: `server.cpp` spawns the separate `llama-connect` binary over WebRTC and references `server_connect` unconditionally) joined upstream's `llama-server-impl` source list and therefore both of ours, next to `server.cpp` (non-Android; `jllama` and `jllama_test`) -- without it the link fails on macOS/Windows and is latent on Linux, the `server-mcp.cpp` class. Nothing bundles the binary: `LLAMA_CONNECT` (new root option, a prebuilt download) stays OFF and `--connect` answers with upstream's "not available" message. `cpp-httplib` **0.62.0** (b11737, #30785): WebSocket support, `server_http_context::ws()` and `ws_handler_t` in `server-http.h` (first used by the realtime API at b11780, next row). `LLAMA_VERSION` 0.7.0 / ggml 0.27.0 (b11669, b11661). `unicode.cpp` rejects overlong UTF-8 and out-of-range code points (b11650, #30485) -- the tokenizer's decoder, not the chat parser's lenient path `ContentOnlyParseUtf8` pins. `common_params::cache_ram_explicit` + `server_task::is_stateless()` (b11642, #30498: only restorable slot states go to the host prompt cache, and a decision model disables that cache unless `--cache-ram` is given), b11664 saves the draft state in a slot file, b11663 prefers empty slots. `SERVER_MODEL_STATUS_INCOMPLETE` (b11714, #29584: a router download that never finished is listed as `"incomplete"`; `RouterModel.Status.INCOMPLETE` follows in the b11799 step). `id_slot` passes through `/v1/messages` (b11756, #28554; the Java route follows in the b11799 step). mtmd: `mtmd_audio_stream` (b11675, #30629, realtime audio input, Voxtral RT), Muse Spark, Nemotron Omni, LFM2 tiling from GGUF; `tools/mtmd/CMakeLists.txt` lists the new model files itself. Upstream's CI: the SYCL release jobs moved from the oneAPI Base Toolkit to the Deep Learning Essentials + oneDNN installers (b11654, #30131) and the Linux one builds with system OpenSSL (b11676, #30456, `LLAMA_OPENSSL=OFF` removed); ours followed at the b11800 bump (the commit after it: both jobs on the pinned DLE + oneDNN installers, `KEEP IN SYNC WITH UPSTREAM`, plus the Level Zero loader/SDK upstream has had since #21597 -- a new `libze_loader.so.1` / `ze_loader.dll` import of both shipped modules and, on Windows, the oneAPI 2026 runtime instead of 2025.3; CHANGELOG); the OpenSSL half does not apply, the SYCL jars being module-only builds without cpp-httplib. ROCm 10.0.0, OpenVINO 2026.4.1, CCCL v3.4.3 and the Vulkan SDK 1.4.357.0 unchanged. No `_DEFAULT` change in the root `CMakeLists.txt`. Server contract: no request field or bound removed or added, no response key removed (`server-schema.cpp` / `server-task.cpp` greps b11639 vs b11760). |
+| b11639–b11760 | patches | **`0016` dropped** (Kolibri-1 is upstream's since b11709, #30673; the applier fails on the new-file collision, see `docs/history/dropped-llama-patches.md`, which also records what the two community GGUF dialects lose), `test_kolibri1.cpp` moved to upstream's converter format (5 -> 3 tests, 666 -> 664) and `REUSE.toml`'s annotation for the patch removed. **The other 13 apply unchanged** (`git apply` in order on a pristine `b68449857` worktree; `check-patches.py`: 13 patches, 0 problems). Drop-checks at b11760, read not grepped: `0014` -- `common/log.h` still declares only `common_log_default_callback`, no hook; `0015` -- `GGML_ABORT("Failed to connect` still in `ggml-rpc.cpp`, no `stop_server`; `0017` -- the four `_mm_prefetch` calls unchanged (`quants.c` 846/847/864/865); `0018` -- `free(buffer->context)` still at `amx.cpp:46` (a hit means the defect stands); `0019` -- the `__ARM_NEON` branch of `simd-mappings.h` still selects no `GGML_CPU_FP32_TO_FP16` (the sed prints nothing), one commit before #30505 lands (b11761, next row); `0020` -- `ggml_gallocr_needs_realloc()` still compares only `n_leafs`; `0021` -- the `else` branch of `launch_slot_with_task()` still assigns `params_base.lora_adapters` without clearing, `init()` still leaves `slot.lora` empty. |
+| b11760–b11761 | `ggml/src/ggml-cpu/simd-mappings.h` (one commit, `119f68d7d` = b11761, 2026-10-20) | **This project's #30505 merged** ("ggml-cpu : select the NEON fp32->fp16 conversion"): the NEON branch selects `GGML_CPU_FP32_TO_FP16`, the `#error` pair guards the chain, the `!defined(__F16C__)` carve-out as submitted; the maintainers shortened the comments, the code lines are the patch's. No source change here. |
+| b11760–b11761 | patches | **`0019` dropped** -- the drop-check prints its one line at b11761 (`#define GGML_CPU_FP32_TO_FP16(x) GGML_CPU_COMPUTE_FP32_TO_FP16(x)` inside the `__ARM_NEON` branch, line 37) and the applier fails on the hunk whose `+` lines are already there; dropped rather than refreshed, as its row said (`docs/history/dropped-llama-patches.md`). **The other 12 apply unchanged** (`git apply` in order on a pristine `119f68d7d` worktree; `check-patches.py`: 12 patches, 0 problems). The other drop-checks are those of the b11760 row: this tag touches no other patch-target file. |
+| b11761–b11799 | upstream verification (38 commits, `0756e83fc` = b11799, 2026-10-20 .. 2026-10-21) | **One compile break, in `tts_engine.cpp`: `tools/mtmd/mtmd-helper.h` moved** (b11796, #30902, "mtmd: add state object for mtmd_gen"): `mtmd_helper::gen_audio` lost its `(lctx, mctx)` constructor for `init(lctx, mctx)` + `valid()`, `mtmd_helper_gen_audio_inp` gained a leading `stream` flag with `mtmd_helper_gen_audio_inp_default()` to fill it, and `flush()` joined for streamed output; `mtmd.h` grew `mtmd_gen_state` (`mtmd_gen_audio_process` takes it, `mtmd_gen_inp` lost `seed`/`state_data`), `mtmd_caps::gen_audio`, and the realtime-input `mtmd_audio_stream` API. `tts_engine.cpp` default-constructs the helper, checks `valid()`, and starts from the defaults (`stream` off) -- the contrary of what CLAUDE.md's TTS section promised ("nothing to re-verify"), corrected there. **Server: two new routes in the common table, hence `0007` refreshed** -- `POST /tts` (b11781, #26603: `SERVER_TASK_TYPE_TTS`, `server_task_result_tts` {`audio`, `sample_rate`, `final`}, `eval_tts_schema` with its own `n_predict` [-1, INT32_MAX] / `top_k` / `top_p` / `seed` fields, `server_context_meta::has_cap_chat` / `has_cap_tts`, `server_model_output_modalities(…, has_cap_chat, has_cap_tts)` which appends `"audio"` for a TTS-capable model -- visible through `ModelMeta.getOutputModalities()` without a change here -- and `common_gguf_info::has_chat_tmpl`) and the WebSocket `/v1/realtime` (b11780, #30636: `SERVER_TASK_TYPE_TRANSCRIBE_RT`, `server_control_action` as an enum where `task_params::control_action` was a string, `control_audio`, `server_context_meta::has_audio_stream`, `server_http_context::ws()` / `ws_handler_t`, the router's `proxy_ws`). `NativeServer` and attach mode serve both; `LlamaModel` and the Java transport expose neither (TODO.md). `/v1/messages`: `extra_params` merged into the converted body last (b11795, #30856) and `id_slot` passed through (b11756) -- both mirrored in `AnthropicApiSupport` with tests. Responses API SSE `output_index` (b11768, #30863; the Java translator already emits it). logprobs with speculative decoding (b11776, #27196). `usage.completion_tokens` with `n > 1` (b11782, #29486; refused here anyway). SWA checkpoint invalidation (b11738, #30666). BoringSSL 0.20261020.0 (b11794, #30889; the local verification build still used the 0.20260929.0 checkout, CI fetches the new tag). `common/download.h`: the sidecar / partial-download API for the router's unfinished downloads (`common_download_sidecars()`, `common_download_plan_partials()`, …; unused here), `server_models` with `unfinished_downloads_*`. `include/llama.h`: a comment (LiliCorr embedding state). Vocabulary loading optimised (b11793, #30679), jinja `unique` filter and hex escapes. Server contract b11639 vs b11799: no request field or bound of the completion schema removed or added (the TTS schema's are new), no response key removed, five added (`content_index`, `final`, `n_bytes`, `output_index`, `sample_rate`). `_DEFAULT` lines of the root `CMakeLists.txt` unchanged; ROCm / OpenVINO / CCCL / Vulkan pins unchanged (see the b11760 row for the SYCL drift). **Verified at b11799**: `jllama` and `jllama_test` built from the pin in a fresh tree (`llama.cpp pin check: fetched commit count 11799 = b11799`), `ctest` **685/685** (664 `TEST` macros, the parameterised `CApiVocabOnly` cases counted per instance), the Kolibri-1 comparison among them against upstream's implementation. |
+| b11761–b11799 | patches | **`0007` refreshed** (the route table gained `/tts` and `/v1/realtime` between the transcription and the Anthropic routes; both lines added to the removal block and to `llama_server_register_common_routes()`, the helper checked by script to be the verbatim copy of the block it replaces; the patch header records it). **The other 11 apply unchanged** (`git apply` in order on a pristine `0756e83fc` worktree; `check-patches.py`: 12 patches, 88 hunks, 0 problems). Drop-checks read at b11799: `0014` -- `common/log.h` still has no hook (only `common_log_default_callback`); `0015` -- `GGML_ABORT("Failed to connect` still at `ggml-rpc.cpp:650`, no `stop_server`; `0017` -- the four `_mm_prefetch` calls unchanged (`quants.c` 846/847/864/865); `0018` -- `free(buffer->context)` still at `amx.cpp:46`; `0020` -- `ggml_gallocr_needs_realloc()` still compares only `n_leafs`; `0021` -- the `else` branch of `launch_slot_with_task()` still assigns `params_base.lora_adapters` without clearing, `init()` still leaves `slot.lora` empty. |
+| b11799–b11800 | `ggml/include/ggml.h`, `ggml/include/ggml-rpc.h` (one commit, `d454a204e` = b11800, 2026-10-22) | **"llama: create new fused op for QSA mask" (#30640)**: `GGML_OP_TOP_K_MASK` / `ggml_top_k_mask(mask, top_k, score, blocks, extra)` -- the top-k sparse-attention mask the k-pool models (qwen4exp, glm5-next), the DSA attention and the DeepSeek V4 CSA mask built by hand before -- with CPU and CUDA kernels, `llm_graph_context::build_fused_top_k_mask` (used when the backend probe enables the op, the hand-built path kept otherwise), and `test-backend-ops` cases. `GGML_OP_COUNT` 101 -> 102, so `RPC_PROTO_PATCH_VERSION` 0 -> 1 (`ggml-rpc.h`, a `0015` target file; the hunk there applies unchanged, and the handshake compares only major and minor, so a b11799 client still talks to a b11800 server). No source change here; nothing in `common/`, `include/`, `tools/`, the root `CMakeLists.txt` or `.github/workflows/release.yml`. **Verified at b11800**: `jllama` and `jllama_test` built from the pin in a fresh tree, `ctest` **685/685** (664 `TEST` macros). |
+| b11799–b11800 | patches | **All 12 apply unchanged** (`git apply` in order on a pristine `d454a204e` worktree; `check-patches.py`: 12 patches, 88 hunks, 0 problems). The commit touches one patch-target file, `ggml-rpc.h` (the version defines above, outside `0015`'s hunk); the drop-checks of the b11799 row stand, every other patch-target file is untouched. |
+| b11800–b11803 | `src/`, `tools/mtmd/`, `common/jinja/value.cpp` (3 commits, `a37cc4610` = b11803, 2026-10-21 .. 2026-10-22) | **Two architectures and a jinja fix, no source change here.** Flint (`FlintForCausalLM`, #30928, b11803): `LLM_ARCH_FLINT`, `src/models/flint.cpp`, pre-tokenizer `flint` (`LLAMA_VOCAB_PRE_TYPE_FLINT` = 62, its own regex), and two shared edits -- `build_amax_src1_f16()` in `src/llama-graph.cpp` scales the row maximum by `1/1024` instead of `1/32768` ("x32 headroom for the F16 sums of 32 values in Q8 activation blocks", a numerical change for every architecture on that path, Mistral 4 / Mellum / Flint), and `src/llama-kv-cache.cpp` applies RoPE to the cache only when some layer has it (`hparams.has_rope(il)`), so an architecture without RoPE in any layer skips it. SenseNova U1 (#28919, b11801): `LLM_ARCH_SENSENOVA_U1`, `src/models/sensenova-u1.cpp`, and its vision projector in mtmd -- `PROJECTOR_TYPE_SENSENOVA_U1` (`tools/mtmd/models/sensenova-u1.cpp`, a new source `tools/mtmd/CMakeLists.txt` lists, so `mtmd` picks it up by itself), ``/`` markers, dynamic-size preprocessing, and `MTMD_POS_TYPE_SENSENOVA_U1` (M-RoPE with zero-based image H/W and one temporal position per image); `mtmd.h` is untouched. jinja (#30941, b11802): `namespace` objects no longer carry the dict built-ins, so an attribute named like a dict method (`ns.items`) resolves to the attribute, as in jinja2 -- a template-rendering change the `--jinja` path inherits. Nothing in `common/*.h`, `include/`, `ggml/`, `tools/server/` or the root `CMakeLists.txt` (`GGML_LLAMAFILE_DEFAULT` / `GGML_CUDA_GRAPHS_DEFAULT` unchanged); the server request-field set, its bounds and the response keys are identical between the tags (the three greps of the runbook); upstream's `.github/` is untouched, so the OpenVINO, CCCL, ROCm and oneAPI pins stay. |
+| b11800–b11803 | patches | **All 12 apply unchanged** (the fail-loud applier on a fresh build directory at `a37cc4610`; `jllama-build-info` lists 0001, 0002, 0003, 0006, 0007, 0008, 0014, 0015, 0017, 0018, 0020, 0021; `check-patches.py`: 12 patches, 88 hunks, 0 problems). No patch-target file is in the range; the drop-checks read at b11803 give the b11799 answers: `0014` -- `common/log.h` names only `common_log_default_callback`; `0015` -- `GGML_ABORT("Failed to connect` still at `ggml-rpc.cpp:650`, no `stop_server`; `0017` -- the four `_mm_prefetch` calls at `quants.c` 846/847/864/865; `0018` -- `free(buffer->context)` still at `amx.cpp:46`; `0020` -- `ggml_gallocr_needs_realloc()` still compares only `n_leafs`; `0021` -- the `else` branch of the per-request LoRA block still takes `params_base.lora_adapters` without the clear; `0001` -- `argv = utf8.ptrs.data()` still at `arg.cpp:1288`, no `common_params_parse_main` in `arg.h`; `0002` -- the server still installs its own `load_progress_callback` unconditionally; `0008` -- `get_server_exec_path()` still the worker binary. |
+| b11803–b11809 | `src/llama-adapter.cpp`, `src/llama-graph.cpp`, `src/llama-model.h`, `src/models/dflash.cpp`, `ggml/src/ggml-opencl/`, `ggml/src/ggml-webgpu/`, `tools/ui/` (6 commits, `0b1281aee` = b11809, 2026-10-21 .. 2026-10-22) | **No source change here.** LoRA on prism.hadamard folded weights (#30910, b11806): `build_lora_mm` / `build_lora_mm_id` feed the folded weight the Hadamard-transformed input and the LoRA delta the primal `cur`, so an adapter on a folded matmul weight is accepted now; only a latent lookup table (`token_embd`, the tied head) is still refused ("targets a prism.hadamard latent lookup table") -- a message `test_lora_cache.cpp`'s random adapter never meets, its random model carrying no Hadamard blocks. dflash (#30945, b11808): an optional `fc.input_scale` tensor (`fc_in_s`, `TENSOR_NOT_REQUIRED`). OpenCL F32-to-I32 ARGMAX kernel (#30897, b11804; `argmax.cl` joins `GGML_OPENCL_KERNELS`, so the embedded-kernel builds of the three OpenCL module jars -- `opencl-android-aarch64`, `opencl-windows-x86-64`, `opencl-windows-aarch64` -- pick it up by themselves), WebGPU Q2_0 (#30914, b11809, not built here), two WebUI commits (#30681, #30919, auto-followed by `build-webui`). Nothing in `common/`, `include/`, `tools/server/`, `tools/mtmd/`, the root `CMakeLists.txt` (`GGML_LLAMAFILE_DEFAULT` / `GGML_CUDA_GRAPHS_DEFAULT` unchanged) or upstream's `.github/`; the server request-field set, its bounds and the response keys are identical (`tools/server/` is not in the range). **Verified at b11809**: `jllama` and `jllama_test` built from the pin in a fresh tree, `ctest` **686/686** (665 `TEST` macros). |
+| b11803–b11809 | patches | **All 12 apply unchanged** (the fail-loud applier on a fresh build directory at `0b1281aee`; `jllama-build-info` lists 0001, 0002, 0003, 0006, 0007, 0008, 0014, 0015, 0017, 0018, 0020, 0021; `check-patches.py`: 12 patches, 88 hunks, 0 problems). No patch-target file is in the range -- `git diff --name-only b11803..b11809` names no `common/arg.*`, `common/log.*`, `tools/server/*`, `ggml-rpc`, `ggml-alloc.c`, `amx.cpp` or `arch/x86/quants.c`, and no `tests/*` or `examples/*` file at all -- so the drop-check answers of the b11803 row stand unchanged. |
+| b11809–b11814 | `src/llama-vocab.cpp`, `src/llama-arch.{cpp,h}`, `src/llama-model-saver.cpp`, `src/models/granite*.cpp`, `ggml/src/ggml-cpu/kleidiai/`, `ggml/src/ggml-feats.h`, `ggml/src/ggml-cpu/CMakeLists.txt`, `ggml/src/ggml-sycl/convert.cpp`, `gguf-py/`, `tests/` (5 commits, `d88040c70` = b11814, 2026-10-21 .. 2026-10-22) | **Two tokenizer behaviour changes, no source change here.** `tokenizer.ggml.clean_spaces` (#30940, b11814): the detokenizer's clean-up of spaces before punctuation (transformers' `clean_up_tokenization_spaces`) is a GGUF key now (`LLM_KV_TOKENIZER_CLEAN_SPACES`, written by the converter from `tokenizer_config.json`, read with `ml.get_key(..., false)` after the per-type defaults -- SPM, UGM and RWKV `false`, WPM `true`, BPE `true` unless the pre-tokenizer branch says otherwise, all unchanged -- so it only overrides when present, and written by the model saver, so a GGUF `LlamaTrainer` writes carries it and `GgufInspector` shows it), and the pre-tokenizer table turns it off where it was on before -- `minicpm5`, `lfm2`, `gemma-4` / `granite-embed-multi-311m`, `dbrx`, `chatglm-bpe`, `glm4` / `glm5`, `exaone4`, `mellum2` -- so the detokenized text of those models keeps the space before punctuation that the old clean-up removed. Pixtral (#30960, b11813): pre-tokenizer `pixtral` moved from the Llama-3 pre-type to Tekken (`LLAMA_VOCAB_PRE_TYPE_TEKKEN`, `clean_spaces = false`), a tokenization change for Pixtral GGUFs. Granite (#30957, b11812): the four granite graphs record each layer's input in `t_layer_inp` (the eagle3 / dflash feature-fusion hook), no output change. KleidiAI v1.32.0 with Q8_0 SVE dotprod GEMV and SVE i8mm GEMM kernels (#30808, b11811; `ggml-feats.h` learns `has_svei8mm`) -- `GGML_CPU_KLEIDIAI` is off in every build here and the aarch64 CPU variant list is unchanged (`ggml/src/ggml-cpu/CMakeLists.txt` differs only in the KleidiAI tag and archive hash). SYCL dequant switches merged (#30740, b11810, `convert.cpp` only). Nothing in `common/`, `include/`, `tools/server/`, `tools/mtmd/`, the root `CMakeLists.txt` (`GGML_LLAMAFILE_DEFAULT` / `GGML_CUDA_GRAPHS_DEFAULT` unchanged) or upstream's `.github/`; the server request-field set, its bounds and the response keys are identical (`tools/server/` is not in the range). None of CI's models uses one of the pre-tokenizers above, and the random fixtures write no `clean_spaces` key and keep their pre-tokenizer's default. **Verified at b11814**: `jllama` and `jllama_test` built from the pin in a fresh tree, `ctest` **686/686** (665 `TEST` macros). |
+| b11809–b11814 | patches | **All 12 apply unchanged** (the fail-loud applier on a fresh build directory at `d88040c70`; `jllama-build-info` lists 0001, 0002, 0003, 0006, 0007, 0008, 0014, 0015, 0017, 0018, 0020, 0021; `check-patches.py`: 12 patches, 88 hunks, 0 problems). The range touches two files under `tests/` (`tests/CMakeLists.txt`, `tests/test-tokenizer-0.cpp`), neither of them a `0001` target (its `tests/` hunks are `test-arg-parser.cpp`, `test-export-graph-ops.cpp` and `test-thread-safety.cpp`), and no other patch-target file, so the drop-check answers of the b11803 row stand unchanged. |
+| b11814–b11815 | `ggml/src/ggml-sycl/conv2d.cpp`, `ggml/src/ggml-sycl/ggml-sycl.cpp`, `docs/ops.md`, `docs/ops/SYCL.csv` (one commit, `7170cad4d` = b11815, 2026-10-22) | **One SYCL kernel fix, no source change here.** `CONV_2D` on the SYCL backend (#30363) supports the channel-fastest CWHN layout (input `[N][H][W][C]`, kernel `[KH][KW][IC][OC]`) next to the contiguous WHCN one -- the kernel is templated on the layout, `supports_op` accepts a non-contiguous input or kernel whose channel stride is the element size, and the former `GGML_ASSERT(p[6] == 0)` ("no cwhn layout support") is gone; the regenerated `docs/ops/SYCL.csv` (7,793 insertions, 6,047 deletions, 2.3 MB of diff) is what pushed this one-commit chunk over the runbook's 100 KiB threshold on its own, so it is a chunk of its own. The SYCL module jars are built in CI only (`sycl-linux-x86-64`, `sycl-windows-x86-64`); the change reaches a consumer running a convolution (a vision projector, Whisper-style audio input) on an Intel GPU with a CWHN tensor, which used to abort. Nothing in `common/`, `include/`, `tools/server/`, `tools/mtmd/`, `src/`, the root `CMakeLists.txt` (`GGML_LLAMAFILE_DEFAULT` / `GGML_CUDA_GRAPHS_DEFAULT` unchanged) or upstream's `.github/`; the server request-field set, its bounds and the response keys are identical (`tools/server/` is not in the range). **Verified at b11815**: `jllama` and `jllama_test` built from the pin in a fresh tree, `ctest` **686/686** (665 `TEST` macros), `NativeLibraryLoadSmokeTest` against the b11815 library. |
+| b11814–b11815 | patches | **All 12 apply unchanged** (the fail-loud applier on a fresh build directory at `7170cad4d`; `jllama-build-info` lists 0001, 0002, 0003, 0006, 0007, 0008, 0014, 0015, 0017, 0018, 0020, 0021; `check-patches.py`: 12 patches, 88 hunks, 0 problems; and `git apply --check` + `git apply` in filename order on a pristine b11815 worktree, every one clean, the 48 paths the patches touch equal to the `diff --git` headers). **Across the whole b11803..b11815 range no patch-target file changed**: the 48 touched paths are the same blobs at both tags (`git rev-parse :`), and the intersection with `git diff --name-only b11803..b11815` (59 files) is empty. The drop-checks, re-read at b11815, give the b11803 answers: `0001` -- `argv = utf8.ptrs.data()` at `common/arg.cpp:1288` inside the count guard, no `common_params_parse_main` anywhere in the tree (75 `common_params_parse(` call sites at both tags, none new); `0002` -- `params_base.load_progress_callback = load_progress_callback;` unguarded at `server-context.cpp:1283`; `0006`/`0007` -- `tools/server/server.cpp` is the same blob, the helper body still byte-identical to the route-table block it replaces; `0008` -- `get_server_exec_path()` at `server-models.cpp:575`, no `LLAMA_SERVER_WORKER_CMD`; `0014` -- `common/log.h` names only `common_log_default_callback`; `0015` -- `GGML_ABORT("Failed to connect` at `ggml-rpc.cpp:650`, no `stop_server`, `RPC_PROTO` 8/0/1 and `GGML_OP_COUNT` 102 at both tags; `0017` -- the four `_mm_prefetch` calls of `quants.c` with the cast still after the addition; `0018` -- `free(buffer->context)` still at `amx.cpp:46`; `0020` -- `ggml_gallocr_needs_realloc()` still compares only `n_leafs`; `0021` -- the `else` branch of the per-request LoRA block still takes `params_base.lora_adapters` without the clear, `init()` still starts a slot without the server's adapters. Root `CMakeLists.txt` the same blob (`GGML_LLAMAFILE_DEFAULT` / `GGML_CUDA_GRAPHS_DEFAULT` `ON` at lines 167 / 171); `.github/workflows/release.yml`, `vendor/cpp-httplib/CMakeLists.txt`, `ggml/CMakeLists.txt`, `ggml/src/CMakeLists.txt`, `tools/server/CMakeLists.txt` and `tools/mtmd/CMakeLists.txt` byte-identical, so every `KEEP IN SYNC WITH UPSTREAM` pin of `publish.yml` stays (ROCm 10.0.0, Vulkan 1.4.357.0, DLE 2026.1.4 + oneDNN 2026.0.2 + Level Zero 1.33.1, CCCL v3.4.3, OpenVINO 2026.4.1, BoringSSL 0.20261020.0). |
diff --git a/docs/patches.md b/docs/patches.md
index 3871c1569..022084899 100644
--- a/docs/patches.md
+++ b/docs/patches.md
@@ -18,7 +18,7 @@ upstream-shaped, and dropped here the moment upstream carries the fix (the drop-
 in its `jllama-build.txt` (`patches=`) and a hash over the set (`patches.sha256=`), so a consumer can
 match a binary to this page.
 
-**13 patches.** Ids as `jllama-build.txt` lists them under `patches=`.
+**12 patches.** Ids as `jllama-build.txt` lists them under `patches=`.
 
 | Id | Patch | Title | Files | Hunks |
 |---|---|---|---:|---:|
@@ -30,11 +30,10 @@ match a binary to this page.
 | 0008 | [`0008-server-models-worker-cmd-override.patch`](#0008-server-models-worker-cmd-overridepatch) | server: let the router spawn its model workers through a host-supplied command (LLAMA_SERVER_WORKER_CMD) | 1 | 1 |
 | 0014 | [`0014-common-log-callback-sink.patch`](#0014-common-log-callback-sinkpatch) | common: add a callback sink to common_log (common_log_set_callback) | 2 | 6 |
 | 0015 | [`0015-rpc-embeddable-client-and-stoppable-server.patch`](#0015-rpc-embeddable-client-and-stoppable-serverpatch) | ggml-rpc: let an embedding host register servers and stop its own server safely | 5 | 24 |
-| 0016 | [`0016-model-kolibri1.patch`](#0016-model-kolibri1patch) | model: add Kolibri-1 (kolibri1) -- carried until upstream supports it | 6 | 7 |
 | 0017 | [`0017-ggml-cpu-x86-prefetch-byte-offset-and-msvc-type.patch`](#0017-ggml-cpu-x86-prefetch-byte-offset-and-msvc-typepatch) | ggml-cpu(x86): byte-wise prefetch address, and a type clang accepts on MSVC targets | 2 | 4 |
 | 0018 | [`0018-ggml-amx-free-the-aligned-buffer.patch`](#0018-ggml-amx-free-the-aligned-bufferpatch) | ggml-cpu(amx): free the AMX weight buffer with ggml_aligned_free(), not free() | 1 | 1 |
-| 0019 | [`0019-ggml-cpu-select-the-neon-fp32-to-fp16-conversion.patch`](#0019-ggml-cpu-select-the-neon-fp32-to-fp16-conversionpatch) | ggml-cpu : select the NEON fp32->fp16 conversion | 1 | 2 |
 | 0020 | [`0020-ggml-alloc-validate-leafs-before-reusing-an-allocation.patch`](#0020-ggml-alloc-validate-leafs-before-reusing-an-allocationpatch) | ggml-alloc : validate the leafs before reusing a graph's allocation | 1 | 1 |
+| 0021 | [`0021-server-clear-the-slot-cache-when-the-server-adapters-change.patch`](#0021-server-clear-the-slot-cache-when-the-server-adapters-changepatch) | server : clear a slot's cache when the server's LoRA adapters change | 1 | 2 |
 
 ## 0001-win32-arg-parse-embed-guard.patch
 
@@ -144,6 +143,10 @@ full llama-server), marks ready immediately and blocks in ctx_http.join() until
 llama_server_request_shutdown(). It serves the caller's server_routes rather than a frame-local one:
 server_routes' constructor registers a sleeping-state callback on the model's task queue that
 outlives the attach call. Applies after 0001 and 0006.
+
+Refreshed at b11799: POST /tts (b11781, #26603) and the WebSocket /v1/realtime (b11780, #30636) joined
+the route table between the transcription and the Anthropic routes; both are in the removed block and
+in the helper, so attach mode serves them too.
 ```
 
 ## 0008-server-models-worker-cmd-override.patch
@@ -245,78 +248,6 @@ src/test/cpp/test_rpc.cpp: it links both new functions and exercises the registr
 stop paths over loopback.
 ```
 
-## 0016-model-kolibri1.patch
-
-**Files (6):** `src/llama-arch.cpp`, `src/llama-arch.h`, `src/llama-model.cpp`, `src/llama-vocab.cpp`, `src/models/kolibri1.cpp`, `src/models/models.h`
-
-The patch's own header, verbatim:
-
-```text
-model: add Kolibri-1 (kolibri1) -- carried until upstream supports it
-
-Aleph Alpha's Kolibri-1 is a 78B-parameter German/English reasoning MoE (3.46B active per
-token, Apache-2.0). Upstream llama.cpp has no support yet; the request is
-https://github.com/ggml-org/llama.cpp/issues/29922 (open, no PR at the time of writing).
-DROP THIS PATCH as soon as upstream registers the architecture -- check on every bump with
-`git grep -n kolibri src/llama-arch.cpp` in the new llama.cpp tag. This one is a clean true/false,
-unlike the drop-checks of 0017, 0018 and 0019: the name reaches that file only through this patch,
-so NO hits means the patch is still needed and any hit means upstream has it. Run it against the
-pristine tag, never against a patched source tree, which would show this patch's own lines.
-Measured at b11639 (a0e6dc738): 0 hits.
-
-Architecture (from Aleph Alpha's own vLLM implementation, the reference used here):
-  - GQA attention with per-head q/k RMSNorm. config.layer_types interleaves sliding-window
-    layers, which use (NEOX) RoPE, with full-attention layers that have no positional encoding.
-  - Sandwich norms: input norm -> attention -> post-attention norm -> residual, then
-    pre-FFN norm -> routed MoE + one ungated shared expert -> post-FFN norm -> residual.
-  - Router: top-k selected on (logits + expert_bias), experts weighted by the unbiased
-    sigmoid(logits), renormalized only if norm_topk_prob (off in Kolibri-1). This is NOT
-    DeepSeek-V3's sigmoid router (selection on sigmoid(logits) + bias), which picks other
-    experts as soon as the bias is non-zero -- so the graph builds the selection itself and
-    hands it to build_moe_ffn (selected_experts_in); build_moe_ffn itself is unchanged.
-
-What the patch touches: the arch name "kolibri1" (llama-arch.{h,cpp}), the model mapping and
-NEOX rope type (llama-model.cpp), the model struct (models/models.h), the new
-src/models/kolibri1.cpp, and the "kolibri1" pre-tokenizer name mapped to Qwen2's
-(llama-vocab.cpp). No converter or gguf-py change: this project only loads GGUFs.
-
-Both published GGUF dialects load. The two community converters disagree on metadata, and
-each community runtime rejects the other's files:
-  - expert_gating_func: 2 (sigmoid) from the AFMoE-based converter, 5 ("sigmoid_logit_add",
-    a value only the Qwen3-MoE-based converter's patch defines) from the other one, or
-    absent. All three mean the same router, which is a property of the architecture.
-  - tokenizer.ggml.pre: "qwen2" or "kolibri1"; the Qwen3-MoE-based runtime registers the
-    latter, the AFMoE-based one does not (llama.cpp rejects an unknown pre-tokenizer).
-  - expert_weights_norm, expert_shared_count and output.weight are optional here.
-
-Known places (all read while writing this patch, 2026-10-06):
-  - Request:   https://github.com/ggml-org/llama.cpp/issues/29922
-  - Model:     https://huggingface.co/Aleph-Alpha/Kolibri-1 (and Kolibri-1-BF16)
-  - Reference: https://github.com/Aleph-Alpha/aleph-alpha-inference, aleph_alpha_inference/
-               kolibri1.py at 049a6a7 (Apache-2.0, Copyright 2026 Aleph Alpha GmbH) -- the
-               specification this patch follows; its tests/test_kolibri1.py pins the router.
-  - AFMoE-based port by Eliasfpv28, base llama.cpp edd6e2bbd (b11378):
-               https://huggingface.co/Eliasfpv28/Kolibri-1-Q3_K_S-GGUF, runtime-source/
-               kolibri1-runtime.patch (new additions Apache-2.0, llama.cpp-derived parts MIT).
-               Source of the graph-side router (selection built outside build_moe_ffn).
-               Its runtime is also what https://huggingface.co/InsidiousFiddler/Kolibri-1-GGUF
-               points to.
-  - Qwen3-MoE-based port, base llama.cpp 836d571 (b11381), as carried in
-               https://github.com/mjwsolo/localcode/pull/101 (patches/0006-kolibri1.patch,
-               "community patch"). https://huggingface.co/Hob-forge/Kolibri-1-GGUF publishes
-               GGUFs and a kolibri1-llama.cpp.patch (MIT) for the same base commit whose model
-               card describes the same design (a new gating mode); that file itself could not be
-               fetched from here, so it is not verified to be byte-identical. Source of the
-               loader (optional keys, tied-output fallback, swa rope base) and of the graph layout.
-  - Independent effort under the name "kolibri" (not "kolibri1"), whose GGUFs this patch
-               does not load: https://github.com/CWBudde/kolibri-llama-cpp,
-               https://github.com/CWBudde/llama.cpp/pull/7
-
-Verification in java-llama.cpp: src/test/cpp/test_kolibri1.cpp builds tiny random Kolibri-1
-GGUFs in both dialects, runs them through the real library and compares every logit with an
-independent float reference written from the vLLM implementation above.
-```
-
 ## 0017-ggml-cpu-x86-prefetch-byte-offset-and-msvc-type.patch
 
 **Files (2):** `ggml/src/ggml-cpu/arch/x86/prefetch.h`, `ggml/src/ggml-cpu/arch/x86/quants.c`
@@ -460,104 +391,6 @@ a hit means the bare free() is STILL THERE and this patch is still needed; it is
 grep prints NOTHING. Measured at b11639 (a0e6dc738): one hit, line 46, so the defect stands.
 ```
 
-## 0019-ggml-cpu-select-the-neon-fp32-to-fp16-conversion.patch
-
-**Files (1):** `ggml/src/ggml-cpu/simd-mappings.h`
-
-The patch's own header, verbatim:
-
-```text
-ggml-cpu : select the NEON fp32->fp16 conversion
-
-The __ARM_NEON branch of ggml/src/ggml-cpu/simd-mappings.h defines both
-GGML_CPU_COMPUTE_FP16_TO_FP32 and GGML_CPU_COMPUTE_FP32_TO_FP16 but selects only the first, so
-GGML_CPU_FP32_TO_FP16 falls through to GGML_COMPUTE_FP32_TO_FP16 at the end of that section --
-ggml-base's deliberately portable software bit manipulation, the one ggml.c uses everywhere
-because ggml-base may not assume CPU features -- while neon_compute_fp32_to_fp16, a one-line
-__fp16 conversion, stays defined and referenced by nothing. The whole purpose of the GGML_CPU_*
-pair is to be the accelerated variant; on ARM it was defined to be the unaccelerated one.
-
-__POWER9_VECTOR__ and __riscv_zfhmin select both macros, and the comment on that fallback has
-always said NEON does too ("so we define GGML_CPU_FP16_TO_FP32 and GGML_CPU_FP32_TO_FP16
-elsewhere for NEON. This is also true for POWER9."). Not a regression: the NEON selection is
-absent in all 25 most recent commits touching the file, back to 2025-04-10.
-
-Why it is felt on ARM and nowhere else: ggml_cpu_fp32_to_fp16 -- the from_float of GGML_TYPE_F16
--- has vector paths for F16C, RISC-V and s390x (the last added in llama.cpp #30157) and none for
-NEON, so there the whole function is the scalar loop through the macro.
-
-THE GUARD, AND WHY ITS FIRST HALF HAS A CARVE-OUT. In place of the stale fallback comment an
-#error pair refuses a branch that defines a COMPUTE macro without selecting it -- the omission
-that neither a build, a warning nor a test could see, because the fallback silently supplies a
-correct implementation, so the only symptom is a hardware instruction nothing ever reaches. The
-FP16->FP32 half carries `&& !defined(__F16C__)`, with the issue link on the condition line,
-because llama.cpp #30343 (upstream commit 685f70894) puts that one direction back on the lookup
-table on purpose: clang lowers the F16C intrinsic into the live accumulator register of the
-quantized dot products and serialises the loop, 45-65 % slower Q8_0/Q4_0 MUL_MAT_ID kernels.
-Without the carve-out the guard would fire on every x86 build with F16C -- here that is 11 of the
-14 x86-64 CPU variant modules. The FP32->FP16 half needs no exception: F16C, NEON (with this
-patch), POWER9 and riscv_zfhmin all select it. Should #30342 ever be resolved and the F16C
-selection come back, the carve-out becomes dead weight and should be deleted; that is what the
-link on that line is for.
-
-The guard's position -- immediately before the fallback rather than directly after the #elif
-chain -- was chosen by a mutation measurement, not by taste: before the fallback it catches 4 of
-4 correctness cases with no false alarms, while directly after the chain it misses one (a COMPUTE
-macro defined after the chain and never selected) and, since #30343, fails every x86 F16C build
-outright, because that commit leaves the F16C selection below the lookup-table declarations.
-
-THIS DIFF APPLIES BOTH AT THE PIN AND ON UPSTREAM MASTER, because it no longer touches the F16C
-block at all. An earlier version of this patch also moved #30270's two F16C selections into the
-__F16C__ branch; that was dropped when #30343 turned the block into a documented exception
-carrying an issue link. At the pinned tag #30343 is not in yet, so the F16C block there still
-selects both directions and the carve-out is inert -- x86 behaviour at the pin is exactly
-upstream's. Once the pin crosses 685f70894 the carve-out becomes load-bearing.
-
-WHY THIS PROJECT CARRIES IT. All 17 ARM artifacts shipped here -- cpu-linux-aarch64 (8 variants),
-cpu-android-aarch64 (7), cpu-windows-aarch64 and metal-macos-aarch64 -- contain no fcvt of any
-form in ggml_cpu_fp32_to_fp16, and neither do upstream's own llama-b11576-bin-ubuntu-arm64 and
--macos-arm64. A plain gcc with no flags defines __ARM_NEON and __ARM_FP16_FORMAT_IEEE on aarch64,
-so there is no opt-in and no way to miss it.
-
-MEASURED. The function runs 2.45x to 13.3x faster with the selection, across five ARM core types
-(Ampere eMAG, Cortex-A55, Neoverse-N1, Neoverse-V2, Apple M1), three operating systems and nine
-compiler builds, with the opposite direction measured in the same binary and run as a control that
-stays within 3.2 % everywhere. Bit-exact: both paths agree on every one of the 2^32 float32 bit
-patterns, differing only in NaN payload -- where the same comparison on x86 F16C gives the same
-numbers, so this brings ARM into agreement with what x86 already ships. It is NOT an inference
-speed-up: perf puts 83.9 % of an F16 pp512 run in tinyBLAS's gemm, which converts inside its own
-microkernel, and 0.03 % in this function. The full record, including the mutation measurement that
-chose the guard's position, is docs/benchmarks/arm-fp16-conversion.md.
-
-Built in upstream's own ARM release configuration (GGML_BACKEND_DL, GGML_CPU_ALL_VARIANTS,
-GGML_NATIVE=OFF): all eight variant modules compile, the guard fires nowhere, fcvt/fcvtn/fcvtn2
-appear in every one of them, reverting in the same tree puts that back to zero, and ctest gives
-52 of 54 with the two failures identical on unpatched master. Verified again with clang -E against
-the pinned source after the rebase: the guard is silent in all five configurations (x86 +F16C+AVX2,
-+F16C+AVX512, sse42, plain x86, NEON), and fires when the NEON selection is removed again.
-
-SUBMITTED UPSTREAM as ggml-org/llama.cpp#30505 (open), rebased onto master and byte-identical to
-this diff.
-
-On a bump, check whether upstream merged it. THE OUTPUT MUST BE READ, NOT TESTED -- run it against
-the branch, so that "nothing" is the answer while the patch is needed:
-
-  git show :ggml/src/ggml-cpu/simd-mappings.h \
-    | sed -n '/^#if defined(__ARM_NEON)/,/^#elif/p' | grep -n GGML_CPU_FP32_TO_FP16
-
-One line printed means upstream merged it; then DROP this patch rather than refreshing it -- the
-fail-loud applier detects "does not apply", never "upstream already fixed this".
-
-Do NOT reduce the looser form
-`git grep -n "GGML_CPU_FP32_TO_FP16(x) GGML_CPU_COMPUTE_FP32_TO_FP16" ggml/src/ggml-cpu/simd-mappings.h`
-to a true/false: it returns THREE hits at every tag so far -- __POWER9_VECTOR__, __riscv_zfhmin and
-the out-of-chain __F16C__ block -- and the first two carry the NEON line's four-space indentation, so
-a `grep -q` reports "upstream fixed it" at every tag and deletes a fix that is still needed.
-Measured at b11639 (a0e6dc738): hits at lines 71, 114 and 150, none of them inside the __ARM_NEON
-branch, while `git apply --check` of this patch is clean -- the two answers contradict each other,
-and the grep is the one that is wrong.
-```
-
 ## 0020-ggml-alloc-validate-leafs-before-reusing-an-allocation.patch
 
 **Files (1):** `ggml/src/ggml-alloc.c`
@@ -604,3 +437,59 @@ and only the leafs moved (it dies with SIGSEGV without this patch), reserve with
 Drop-check on every bump: read ggml_gallocr_needs_realloc() in ggml/src/ggml-alloc.c at the new
 tag -- once it walks graph->leafs itself, drop this patch rather than refreshing it.
 ```
+
+## 0021-server-clear-the-slot-cache-when-the-server-adapters-change.patch
+
+**Files (1):** `tools/server/server-context.cpp`
+
+The patch's own header, verbatim:
+
+```text
+server : clear a slot's cache when the server's LoRA adapters change
+
+A slot's prompt cache holds KV entries computed under the LoRA adapters the slot ran with.
+launch_slot_with_task() respects that for a request that names its own adapters: when they differ
+from the slot's, lora_should_clear_cache() decides, and the cache is cleared unless only aLoRA
+adapters are involved. A request that names none takes params_base.lora_adapters in the else
+branch -- without that check. So the cache survives two changes of the adapters it was computed
+with:
+
+  - SET_LORA (POST /lora-adapters, and the embedding API's set-adapters call) replaces
+    params_base.lora_adapters, and the next request on a warm slot reuses its prefix;
+  - a request with its own adapters fills the cache, and the next request without any runs with
+    the server's adapters on top of it.
+
+The slot then goes on with KV entries computed under the old scales and answers like neither the
+old nor the new adapter set, silently. Measured at b11639 with the pair from the server's own LoRA
+test (stories15M_MOE-F16.gguf + moe_shakespeare15M.gguf, greedy, one slot, prompt "Look in thy
+glass"): after setting the adapter's scale from 1 to 0, the warm slot answered ". He saw a picture
+of a bird ..." where the same model with the adapter at 0 on an emptied slot answers " was a little
+girl. She was only three years old ..." -- and erasing the slot was the only way back.
+
+The patch applies the per-request rule in the else branch too: when params_base.lora_adapters
+differ from the slot's, lora_should_clear_cache() decides, the same trace lines are logged, and
+the slot takes the server's adapters. Equal sets keep the cache exactly as before.
+
+For that to hold on a slot's first task, init() starts every slot with the server's adapters.
+slot.lora used to start empty, so a never-used slot of a server started with --lora read as
+"computed under no adapters": its first task logged "0 loras -> 1 loras" and cleared a prompt
+the slot already held -- a state restored into it (SLOT_RESTORE) or loaded from the host-memory
+prompt cache by get_available_slot(), both computed under the server's adapters. The same holds
+for upstream's per-request branch when a request names the server's own adapters. A SET_LORA
+before a slot's first task is still a change and clears, which costs nothing on an empty slot.
+
+Not covered: the host-memory prompt cache (--cache-ram) stores slot states without the adapters
+they were computed with, so a state saved under other adapters can still be loaded into a slot;
+that holds for per-request adapters as much as for these.
+
+The guard is src/test/cpp/test_lora_cache.cpp: a random llama model and a random LoRA adapter on
+K, V and the FFN output, greedy, one slot; after each kind of change the warm slot must answer
+like an emptied one. A second fixture with two slots restores slot 0's saved state into the
+never-used slot 1, which must reuse it.
+
+Drop-check on every bump: read the else branch of the "process per-request lora adapters" block of
+launch_slot_with_task() in tools/server/server-context.cpp at the new tag -- once it compares
+params_base.lora_adapters with slot.lora and clears the prompt (or SET_LORA clears the slots),
+drop that hunk rather than refreshing it; and read the slot loop of init() -- once it sets
+slot.lora from params_base.lora_adapters, drop that hunk. The patch goes when both have.
+```
diff --git a/docs/upgrade/llama-cpp-version-bump.md b/docs/upgrade/llama-cpp-version-bump.md
index ea2dbd4ad..93bdd8e79 100644
--- a/docs/upgrade/llama-cpp-version-bump.md
+++ b/docs/upgrade/llama-cpp-version-bump.md
@@ -139,6 +139,10 @@ for each:
   both CUDA builds pass the same value, see CLAUDE.md, "Upgrading CUDA Version".
 - **ROCm** -- the TheRock wheel version of upstream's `ubuntu-rocm` / `windows-rocm` release jobs (see
   CLAUDE.md, "Additional GPU-backend natives").
+- **SYCL (oneAPI)** -- the installer URLs of upstream's `ubuntu-24-sycl` / `windows-sycl` release jobs (Deep
+  Learning Essentials, oneDNN, the Level Zero release) and their component lists; both SYCL jobs here carry
+  them as `ONEAPI_DLE_URL` / `ONEAPI_ONEDNN_URL` / `LEVEL_ZERO_*` under `KEEP IN SYNC WITH UPSTREAM`. Intel
+  publishes no checksum next to the files, so the URL is the pin.
 
 ---
 
diff --git a/llama-atmosphere-agent/README.md b/llama-atmosphere-agent/README.md
index c93595c10..d577de206 100644
--- a/llama-atmosphere-agent/README.md
+++ b/llama-atmosphere-agent/README.md
@@ -126,7 +126,7 @@ natives table lists every backend.
    ```bash
    jbang --main net.ladenthin.llama.server.ServerLauncher \
        --deps net.ladenthin:llama:6.0.0:cpu-linux-x86-64 net.ladenthin:llama:6.0.0 \
-       -m models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf --jinja --port 8080
+       -m models/Qwen3-4B-Instruct-2507-Q4_K_M.gguf --jinja --port 9931
    ```
 
    The classes jar `net.ladenthin:llama` plus the CPU natives jar of the platform (`cpu--`,
@@ -142,7 +142,7 @@ natives table lists every backend.
 
    ```bash
    mvn -q compile exec:java \
-       -Dexec.args="--base-url http://127.0.0.1:8080/v1 --workspace /path/to/project --allow-shell"
+       -Dexec.args="--base-url http://127.0.0.1:9931/v1 --workspace /path/to/project --allow-shell"
    ```
 
 A `you>` prompt appears, with a status line pinned to the bottom of the window. The answer streams as it is generated, and every
@@ -153,7 +153,7 @@ A single turn without the REPL:
 
 ```bash
 mvn -q compile exec:java \
-    -Dexec.args="--base-url http://127.0.0.1:8080/v1 --workspace /path/to/project --prompt 'Read the README and summarize it'"
+    -Dexec.args="--base-url http://127.0.0.1:9931/v1 --workspace /path/to/project --prompt 'Read the README and summarize it'"
 ```
 
 If the server was started with `--api-key `, add `--api-key ` to the agent's arguments.
@@ -235,7 +235,7 @@ pinned block and no spinner, and nothing on screen is ever rewritten. That makes
 when it is piped, logged, recorded, or carried by anything that forwards lines rather than a screen:
 
 ```bash
-mvn -q compile exec:java -Dexec.args="--base-url http://127.0.0.1:8080/v1 --plain" | tee session.log
+mvn -q compile exec:java -Dexec.args="--base-url http://127.0.0.1:9931/v1 --plain" | tee session.log
 ```
 
 The same console is what the agent falls back to on its own when there is no usable terminal — a pipe,
@@ -838,7 +838,7 @@ into its answer instead of calling the tool.
 | `GET /v1/models` | yes | optional (best-effort enumeration) | wire | none |
 | API key | `--api-key` → `401` on mismatch | sends `Authorization: Bearer`; a dummy key is fine | wire | none |
 | custom base URL | — | `LLM_BASE_URL` / `AiConfig.configure(...)` | wire + model | none |
-| error before the stream starts (401, 413, 500) | HTTP status + JSON error | surfaced as `session.error` (5xx retried) | wire | none |
+| error before the stream starts (401, 413, 500, and an engine error before the first chunk) | HTTP status + JSON error | surfaced as `session.error` (5xx retried) | wire | none |
 | **engine error after the stream started** | HTTP 200 already sent → `data: {"error":…}`, no `[DONE]` | **ignored**: the SSE parser reads only `choices[0]`, the turn completes with the text so far | wire (pinned as a known gap) | SHOULD, in Atmosphere's `OpenAiCompatibleClient` |
 
 *wire* = `AtmosphereWireContractTest` / `LocalAgentTest`: the **real** `OpenAiCompatServer` (routing,
diff --git a/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/AgentOptions.java b/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/AgentOptions.java
index 50301cc33..8d72fbb39 100644
--- a/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/AgentOptions.java
+++ b/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/AgentOptions.java
@@ -125,8 +125,10 @@ private AgentOptions(Builder b) {
      *
      * @param args the raw arguments
      * @return the parsed options
-     * @throws IllegalArgumentException on an unknown flag, a missing value, or when neither/both of
-     *     {@code --base-url} and {@code --model} are given
+     * @throws IllegalArgumentException on an unknown flag, a missing value, when neither/both of
+     *     {@code --base-url} and {@code --model} are given, or when {@code --model} is given with a
+     *     {@code --model-id} that llama.cpp's {@code --alias} would split or strip (a comma, empty,
+     *     leading or trailing whitespace)
      */
     public static AgentOptions parse(String[] args) {
         Builder b = new Builder();
@@ -173,6 +175,9 @@ public static AgentOptions parse(String[] args) {
             if (frontEnds > 1) {
                 throw new IllegalArgumentException("--web, --acp and --prompt exclude each other");
             }
+            if (b.modelPath != null) {
+                requireAliasSafeModelId(b.modelId);
+            }
         }
         return new AgentOptions(b);
     }
@@ -216,6 +221,25 @@ public AgentOptions withWorkspace(Path newWorkspace) {
         return new AgentOptions(b);
     }
 
+    /**
+     * The in-process model is served under the model id as its {@code --alias}, which llama.cpp splits at
+     * commas and strips; an id it would change would make {@code /v1/models} and the answers name the model
+     * differently. The same rule as {@code OpenAiServerCli}'s {@code --model-id}. Against {@code --base-url}
+     * the id names a model of another server and is sent unchanged.
+     */
+    private static void requireAliasSafeModelId(String modelId) {
+        if (modelId.indexOf(',') >= 0) {
+            throw new IllegalArgumentException(
+                    "--model-id must not contain a comma with --model (llama.cpp reads it as a list of aliases), got: "
+                            + modelId);
+        }
+        if (modelId.isEmpty() || !modelId.equals(modelId.trim())) {
+            throw new IllegalArgumentException(
+                    "--model-id must be non-empty without leading or trailing whitespace with --model, got: '" + modelId
+                            + "'");
+        }
+    }
+
     private static String value(String[] args, int index, String flag) {
         if (index >= args.length) {
             throw new IllegalArgumentException("Missing value for " + flag);
@@ -284,7 +308,7 @@ public static String usage() {
                 "",
                 "Endpoint (exactly one):",
                 "  --base-url         OpenAI-compatible base URL of a running server,",
-                "                          e.g. http://127.0.0.1:8080/v1 (java-llama.cpp NativeServer",
+                "                          e.g. http://127.0.0.1:9931/v1 (java-llama.cpp NativeServer",
                 "                          started with --jinja, OpenAiCompatServer, or llama-server)",
                 "  --model      load this GGUF in-process and serve it to the agent",
                 "  --ngl                GPU layers for --model (default 0 = CPU only)",
diff --git a/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/AgentRunner.java b/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/AgentRunner.java
index 574b420f0..49a4b8dd6 100644
--- a/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/AgentRunner.java
+++ b/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/AgentRunner.java
@@ -46,7 +46,7 @@ public final class AgentRunner {
     /**
      * Configure the runtime for one endpoint.
      *
-     * @param baseUrl the OpenAI-compatible base URL, e.g. {@code http://127.0.0.1:8080/v1}
+     * @param baseUrl the OpenAI-compatible base URL, e.g. {@code http://127.0.0.1:9931/v1}
      * @param apiKey the bearer token (sent as {@code Authorization: Bearer ...})
      * @param modelId the model id carried in every request
      * @param tools the tools offered to the model on every turn
diff --git a/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/LocalAgent.java b/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/LocalAgent.java
index 96f4c3414..b86c110c2 100644
--- a/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/LocalAgent.java
+++ b/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/LocalAgent.java
@@ -27,7 +27,7 @@
  * 

Two ways to reach a model: * *

    - *
  • {@code --base-url http://127.0.0.1:8080/v1} — a server you started yourself (java-llama.cpp's fat + *
  • {@code --base-url http://127.0.0.1:9931/v1} — a server you started yourself (java-llama.cpp's fat * jar {@code NativeServer} with {@code --jinja}, its {@code OpenAiCompatServer}, or upstream * {@code llama-server}), so you keep full control over model parameters. *
  • {@code --model model.gguf} — loads the GGUF in this JVM and serves it to the agent over a loopback diff --git a/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/ModelEndpoint.java b/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/ModelEndpoint.java index 2692c15cc..e27be5016 100644 --- a/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/ModelEndpoint.java +++ b/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/ModelEndpoint.java @@ -85,6 +85,9 @@ static ModelParameters modelParameters(AgentOptions options) { .setCtxSize(options.getCtxSize()) .setGpuLayers(options.getGpuLayers()) .setFit(false) + // Without an alias llama.cpp names every answer after the model's path; the alias makes the + // answers carry the id /v1/models lists. + .setAlias(options.getModelId()) // Jinja rendering is what lets the native parser apply the model's tool-call template. .enableJinja(); // llama.cpp logs to stderr, which shares the console with the streamed answer on stdout; the @@ -103,7 +106,7 @@ static ModelParameters modelParameters(AgentOptions options) { /** * The OpenAI-compatible base URL. * - * @return e.g. {@code http://127.0.0.1:8080/v1} + * @return e.g. {@code http://127.0.0.1:9931/v1} */ public String baseUrl() { return baseUrl; diff --git a/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/ServerProps.java b/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/ServerProps.java index 32e21bf5b..174078020 100644 --- a/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/ServerProps.java +++ b/llama-atmosphere-agent/src/main/java/net/ladenthin/llama/atmosphere/ServerProps.java @@ -34,7 +34,7 @@ private ServerProps() {} /** * Look the context size up. * - * @param baseUrl the OpenAI-compatible base URL, e.g. {@code http://127.0.0.1:8080/v1} + * @param baseUrl the OpenAI-compatible base URL, e.g. {@code http://127.0.0.1:9931/v1} * @param apiKey the bearer token to send * @return the context size in tokens, or {@link StatusLine#UNKNOWN_CONTEXT} when it cannot be read */ diff --git a/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/AgentOptionsTest.java b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/AgentOptionsTest.java index 6d9a37da5..91cf2a9cf 100644 --- a/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/AgentOptionsTest.java +++ b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/AgentOptionsTest.java @@ -37,9 +37,9 @@ void plainChoosesTheLineOrientedConsoleEvenWithATerminal() { @Test void baseUrlModeWithDefaults() { - AgentOptions options = AgentOptions.parse(new String[] {"--base-url", "http://127.0.0.1:8080/v1/"}); + AgentOptions options = AgentOptions.parse(new String[] {"--base-url", "http://127.0.0.1:9931/v1/"}); - assertThat(options.getBaseUrl(), is("http://127.0.0.1:8080/v1")); + assertThat(options.getBaseUrl(), is("http://127.0.0.1:9931/v1")); assertThat(options.getModelPath(), is(nullValue())); assertThat(options.getApiKey(), is(AgentOptions.DEFAULT_API_KEY)); assertThat(options.getModelId(), is(AgentOptions.DEFAULT_MODEL_ID)); @@ -262,4 +262,80 @@ void frontEndsExcludeEachOtherAndABadTokenOrPortIsRefused() { IllegalArgumentException.class, () -> AgentOptions.parse(new String[] {"--base-url", "u", "--web-port", "70000"})); } + + @Test + void modelIdThatLlamaCppsAliasWouldSplitOrTrimIsRejectedForAnInProcessModel() { + // The in-process model is served under the id as its --alias, which llama.cpp splits at commas + // and strips; /v1/models and the answers would name it differently again. + for (String id : new String[] {"a,b", " x", "x ", ""}) { + assertThat( + id, + assertThrows( + IllegalArgumentException.class, + () -> AgentOptions.parse(new String[] {"--model-id", id, "--model", "m.gguf"})) + .getMessage(), + containsString("--model-id")); + } + } + + @Test + void modelIdForARemoteEndpointIsSentUnchanged() { + // Against --base-url the id names a model of someone else's server; nothing here splits it. + assertThat( + AgentOptions.parse(new String[] {"--base-url", "http://x/v1", "--model-id", "a,b"}) + .getModelId(), + is("a,b")); + } + /** + * {@code withWorkspace} copies every option and replaces only the workspace -- an editor session + * ({@code --acp}) runs with all of the command line's settings in the project the editor names. The + * copy is written out field by field, so the check is reflective: every field is set to a value no + * parse produces, and a field added later that the copy forgets fails here. + */ + @Test + void withWorkspaceKeepsEveryOtherOption() throws ReflectiveOperationException { + AgentOptions options = AgentOptions.parse(new String[] {"--base-url", "http://localhost:1/v1"}); + java.util.List fields = new java.util.ArrayList<>(); + int n = 0; + for (java.lang.reflect.Field field : AgentOptions.class.getDeclaredFields()) { + if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) { + continue; + } + field.setAccessible(true); + field.set(options, unusualValue(field, n++)); + fields.add(field); + } + Path elsewhere = Paths.get("elsewhere").toAbsolutePath().normalize(); + + AgentOptions copy = options.withWorkspace(elsewhere); + + assertThat(copy.getWorkspace(), is(elsewhere)); + for (java.lang.reflect.Field field : fields) { + if (!field.getName().equals("workspace")) { + assertThat(field.getName(), field.get(copy), is(field.get(options))); + } + } + } + + /** A value of the field's type that no parse produces, distinct per field. */ + private static Object unusualValue(java.lang.reflect.Field field, int n) { + Class type = field.getType(); + if (type == String.class) { + return "unusual-" + field.getName(); + } + if (type == Path.class) { + return Paths.get("/unusual-" + field.getName()); + } + if (type == int.class) { + return 7001 + n; + } + if (type == double.class) { + return 0.5 + n; + } + if (type == boolean.class) { + // every boolean option defaults to false except autoCompact; flip whatever it is + return !field.getName().equals("autoCompact"); + } + throw new AssertionError("no unusual value for " + field + "; add one for its type"); + } } diff --git a/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/AtmosphereWireContractTest.java b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/AtmosphereWireContractTest.java index cfb14b82e..6f75e9da1 100644 --- a/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/AtmosphereWireContractTest.java +++ b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/AtmosphereWireContractTest.java @@ -6,6 +6,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; @@ -259,19 +260,70 @@ void wrongApiKeyIsRejectedBeforeReachingTheBackend() throws Exception { } } + /** + * A request body over the server's cap is answered {@code 413} before the backend is reached, and + * Atmosphere reports it as the turn's failure. The system prompt alone exceeds the 64-byte cap. + */ + @Test + void oversizedRequestIsRejectedBeforeReachingTheBackend() throws Exception { + ScriptedBackend backend = new ScriptedBackend((call, request) -> ScriptedBackend.textTurn("never")); + OpenAiServerConfig smallBodies = OpenAiServerConfig.builder() + .host("127.0.0.1") + .port(0) + .apiKey(API_KEY) + .modelId(MODEL_ID) + .maxRequestBodyBytes(64) + .build(); + try (OpenAiCompatServer server = new OpenAiCompatServer(backend, smallBodies).start()) { + AgentRunner runner = runner(server, API_KEY, List.of()); + ConsoleSession session = session(); + + runner.run("hello", List.of(), session); + + assertThat(session.await(TIMEOUT), is(true)); + assertThat(session.text(), is("")); + assertThat(session.failure(), is(notNullValue())); + assertThat(session.failure().getMessage(), containsString("Request body too large")); + assertThat(backend.requests(), is(empty())); + } + } + + /** + * An engine failure before the first chunk is an error status, as upstream llama-server answers it + * (it reads the first result before it starts a stream), and Atmosphere reports it as the turn's + * failure. Until F182 this server sent HTTP 200 first, and the turn ended as a silent, empty answer. + */ + @Test + void engineFailureBeforeTheFirstChunkFailsTheTurn() throws Exception { + ScriptedBackend backend = new ScriptedBackend((call, request) -> { + throw new IllegalStateException("model exploded"); + }); + try (OpenAiCompatServer server = new OpenAiCompatServer(backend, config(API_KEY)).start()) { + AgentRunner runner = runner(server, API_KEY, List.of()); + ConsoleSession session = session(); + + runner.run("hello", List.of(), session); + + assertThat(session.await(TIMEOUT), is(true)); + assertThat(session.text(), is("")); + assertThat(session.failure(), is(notNullValue())); + assertThat(session.failure().getMessage(), containsString("model exploded")); + } + } + /** * Known gap, pinned so a change on either side is noticed. When the engine fails after the stream * started, java-llama.cpp (like upstream llama-server) has already sent HTTP 200 and reports the * failure as an SSE {@code data: {"error":{...}}} object without a terminating {@code [DONE]}. * Atmosphere's SSE parser only reads {@code choices[0]}, so it ignores that object and completes - * the session normally with whatever text arrived before — an empty answer here, not an error. - * An in-stream error event would be a "SHOULD" for Atmosphere's {@code OpenAiCompatibleClient}. + * the session normally with the text that arrived before. An in-stream error event would be a + * "SHOULD" for Atmosphere's {@code OpenAiCompatibleClient}. */ @Test void midStreamEngineFailureCompletesSilentlyRatherThanErroring() throws Exception { - ScriptedBackend backend = new ScriptedBackend((call, request) -> { - throw new IllegalStateException("model exploded"); - }); + ScriptedBackend backend = new ScriptedBackend( + (call, request) -> List.of(ScriptedBackend.roleChunk(), ScriptedBackend.textChunk("partial")), + new IllegalStateException("model exploded")); try (OpenAiCompatServer server = new OpenAiCompatServer(backend, config(API_KEY)).start()) { AgentRunner runner = runner(server, API_KEY, List.of()); ConsoleSession session = session(); @@ -280,7 +332,7 @@ void midStreamEngineFailureCompletesSilentlyRatherThanErroring() throws Exceptio assertThat(session.await(TIMEOUT), is(true)); assertThat(backend.requests(), hasSize(1)); - assertThat(session.text(), is("")); + assertThat(session.text(), is("partial")); assertThat("Atmosphere does not surface an in-stream error object", session.failure(), is(nullValue())); } } diff --git a/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/LocalAgentTest.java b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/LocalAgentTest.java index 2cce11f07..487281dda 100644 --- a/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/LocalAgentTest.java +++ b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/LocalAgentTest.java @@ -415,6 +415,23 @@ void inProcessModelIsLoadedWithAQuietLogThresholdByDefault() { assertThat(args, not(hasItem("--verbose"))); } + @Test + void inProcessModelAnswersUnderTheModelIdItIsListedWith() { + // Without an alias llama.cpp names every answer and stream chunk after the model's path, while + // /v1/models lists the model id; the alias makes the two agree. + List byDefault = + List.of(ModelEndpoint.modelParameters(AgentOptions.parse(new String[] {"--model", "m.gguf"})) + .toArray()); + List explicit = List.of( + ModelEndpoint.modelParameters(AgentOptions.parse(new String[] {"--model", "m.gguf", "--model-id", "x"})) + .toArray()); + + assertThat(byDefault, hasItem("--alias")); + assertThat(byDefault.get(byDefault.indexOf("--alias") + 1), is(AgentOptions.DEFAULT_MODEL_ID)); + assertThat(explicit, hasItem("--alias")); + assertThat(explicit.get(explicit.indexOf("--alias") + 1), is("x")); + } + @Test void verboseReplacesTheThresholdWithLlamaCppsOwnVerboseFlag() { List args = List.of(ModelEndpoint.modelParameters( diff --git a/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/RetryPolicyWireTest.java b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/RetryPolicyWireTest.java new file mode 100644 index 000000000..6d269721c --- /dev/null +++ b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/RetryPolicyWireTest.java @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2026 Bernard Ladenthin +// +// SPDX-License-Identifier: MIT + +package net.ladenthin.llama.atmosphere; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.atmosphere.ai.RetryPolicy; +import org.atmosphere.ai.fs.AgentFileSystem; +import org.atmosphere.ai.fs.WorkspaceAgentFileSystem; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * What the agent's HTTP retry policy does with an error status, against an endpoint that answers every + * chat request with the same status and counts them. + * + *

    The other wire tests run with {@link RetryPolicy#NONE}, so nothing there tells a {@code 4xx} apart + * from a {@code 5xx}. The agent itself runs with {@link RetryPolicy#DEFAULT}; the policy here keeps + * DEFAULT's set of retryable error types and only sets the delays to zero, so the counts are exact and + * no test waits for a backoff. A request the server refuses as the caller's fault ({@code 400}) must + * not be sent again; an unavailable server ({@code 503}) is asked again until the retries are used up, + * and the turn then ends with the error rather than hanging. + */ +class RetryPolicyWireTest { + + private static final String MODEL_ID = "local-model"; + private static final Duration TIMEOUT = Duration.ofSeconds(30); + private static final int MAX_RETRIES = 2; + private static final String CHAT_PATH = "/v1/chat/completions"; + + /** DEFAULT's retryable error types, without its one-second backoff. */ + private static final RetryPolicy IMMEDIATE_RETRIES = + new RetryPolicy(MAX_RETRIES, Duration.ZERO, Duration.ZERO, 1.0, RetryPolicy.DEFAULT.retryableErrors()); + + @TempDir + Path workspace; + + private HttpServer server; + private final AtomicInteger chatRequests = new AtomicInteger(); + + @AfterEach + void stopServer() { + if (server != null) { + server.stop(0); + } + } + + /** Answers every chat request with {@code status} and an OpenAI-style error body; 404 elsewhere. */ + private String startServerAnswering(int status) throws IOException { + server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0); + server.createContext("/", exchange -> { + try (HttpExchange e = exchange) { + e.getRequestBody().readAllBytes(); + boolean chat = "POST".equals(e.getRequestMethod()) + && CHAT_PATH.equals(e.getRequestURI().getPath()); + if (chat) { + chatRequests.incrementAndGet(); + } + int code = chat ? status : 404; + byte[] body = ("{\"error\":{\"code\":" + code + ",\"message\":\"status " + code + + "\",\"type\":\"test_error\"}}") + .getBytes(StandardCharsets.UTF_8); + e.getResponseHeaders().set("Content-Type", "application/json"); + e.sendResponseHeaders(code, body.length); + try (OutputStream out = e.getResponseBody()) { + out.write(body); + } + } + }); + server.start(); + return "http://127.0.0.1:" + server.getAddress().getPort() + "/v1"; + } + + private ConsoleSession runTurn(String baseUrl) throws InterruptedException { + AgentRunner runner = new AgentRunner(baseUrl, "k", MODEL_ID, List.of(), "You are a test agent.", 0.0, 64, 4) + .retryPolicy(IMMEDIATE_RETRIES); + AgentFileSystem fs = new WorkspaceAgentFileSystem(workspace, AgentFileSystem.Limits.defaults()); + ConsoleSession session = new ConsoleSession( + new PlainTerminal( + new PrintStream(new ByteArrayOutputStream(), true, StandardCharsets.UTF_8), null, Ansi.PLAIN), + fs); + runner.run("Hello", List.of(), session); + assertThat("the turn ends", session.await(TIMEOUT), is(true)); + return session; + } + + @Test + void aBadRequestIsSentExactlyOnce() throws Exception { + String baseUrl = startServerAnswering(400); + + ConsoleSession session = runTurn(baseUrl); + + assertThat(chatRequests.get(), is(1)); + assertThat(session.failure(), is(notNullValue())); + assertThat(String.valueOf(session.failure().getMessage()), containsString("400")); + } + + @Test + void anUnavailableServerIsAskedAgainUntilTheRetriesAreUsedUp() throws Exception { + String baseUrl = startServerAnswering(503); + + ConsoleSession session = runTurn(baseUrl); + + assertThat(chatRequests.get(), is(1 + MAX_RETRIES)); + assertThat(session.failure(), is(notNullValue())); + assertThat(String.valueOf(session.failure().getMessage()), containsString("503")); + } +} diff --git a/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/ScriptedBackend.java b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/ScriptedBackend.java index f638dd11e..2b145ad7f 100644 --- a/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/ScriptedBackend.java +++ b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/ScriptedBackend.java @@ -11,6 +11,7 @@ import java.util.List; import net.ladenthin.llama.server.ChunkSink; import net.ladenthin.llama.server.OpenAiBackend; +import org.jspecify.annotations.Nullable; /** * A model-free {@link OpenAiBackend} that answers each streaming chat request with a scripted sequence @@ -28,10 +29,17 @@ interface Script { } private final Script script; + private final @Nullable RuntimeException afterTheChunks; private final List requests = Collections.synchronizedList(new ArrayList<>()); ScriptedBackend(Script script) { + this(script, null); + } + + /** A backend that sends the scripted chunks and then fails with {@code afterTheChunks}. */ + ScriptedBackend(Script script, @Nullable RuntimeException afterTheChunks) { this.script = script; + this.afterTheChunks = afterTheChunks; } /** Every {@code /v1/chat/completions} body received, in order. */ @@ -45,6 +53,9 @@ public void stream(JsonNode request, ChunkSink sink) throws IOException { for (String chunk : script.chunksFor(requests.size(), request)) { sink.accept(chunk); } + if (afterTheChunks != null) { + throw afterTheChunks; + } } @Override @@ -121,6 +132,22 @@ static List textTurn(String... pieces) { return chunks; } + /** + * A JSON string literal for {@code s}. + * + *

    Every control character has to be escaped, not just the newline. RFC 8259 forbids + * U+0000..U+001F unescaped inside a string, and a chunk that carries one is not JSON: the client + * drops the stream and the turn produces nothing, so what fails is an assertion about the answer, + * several layers from the cause. It escaped only the quote, the backslash and {@code \n}, which + * left 31 of the 32 control characters producing invalid JSON (measured) -- and that made + * every test feeding it {@link System#lineSeparator()} fail on Windows and nowhere else: + * {@code AgentSessionTest.aLoopRunsItsStepsThroughTheFrontEndUntilTheMarker} deterministically, + * with a raw CR inside the chunk, {@code Illegal unquoted character ((CTRL-CHAR, code 13))} behind + * the client's {@code HTTP/1.1 header parser received no bytes}, and a loop that stalled instead of + * reaching its sentinel. The three agent jobs run on Linux, where the separator is a bare LF, so CI + * never saw it. The default branch below is the load-bearing part: a control character nobody + * listed cannot reintroduce the gap. {@link ScriptedBackendTest} is the guard. + */ private static String quote(String s) { StringBuilder sb = new StringBuilder("\""); for (char c : s.toCharArray()) { @@ -128,7 +155,17 @@ private static String quote(String s) { case '"' -> sb.append("\\\""); case '\\' -> sb.append("\\\\"); case '\n' -> sb.append("\\n"); - default -> sb.append(c); + case '\r' -> sb.append("\\r"); + case '\t' -> sb.append("\\t"); + case '\b' -> sb.append("\\b"); + case '\f' -> sb.append("\\f"); + default -> { + if (c < 0x20) { + sb.append(String.format("\\u%04x", (int) c)); + } else { + sb.append(c); + } + } } } return sb.append('"').toString(); diff --git a/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/ScriptedBackendTest.java b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/ScriptedBackendTest.java new file mode 100644 index 000000000..8756e10e0 --- /dev/null +++ b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/ScriptedBackendTest.java @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2026 Bernard Ladenthin +// +// SPDX-License-Identifier: MIT + +package net.ladenthin.llama.atmosphere; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * The scripted backend's own chunks must be JSON, which is a property the tests driving it cannot see: + * an invalid chunk reaches the client as a dropped stream, so the turn simply produces nothing and the + * assertion that fails is about the answer, several layers away from the cause. + * + *

    That is what happened. {@code quote} escaped only {@code "}, {@code \} and {@code \n}, so a text + * carrying {@code System.lineSeparator()} put a raw CR inside the JSON string on Windows -- + * {@code Illegal unquoted character ((CTRL-CHAR, code 13))}, behind the client's + * {@code HTTP/1.1 header parser received no bytes} -- + * and {@link AgentSessionTest#aLoopRunsItsStepsThroughTheFrontEndUntilTheMarker()} failed + * deterministically with a stalled loop. The three agent jobs run on Linux, where the separator is a + * bare LF, so CI never saw it. + */ +class ScriptedBackendTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static void assertEveryChunkIsJson(String what, List chunks) { + assertThat("a turn is at least role + text + finish", chunks.size() >= 3, is(true)); + for (String chunk : chunks) { + try { + MAPPER.readTree(chunk); + } catch (Exception e) { + throw new AssertionError(what + ": chunk is not JSON: " + chunk, e); + } + } + } + + /** The text a turn's chunks carry: the {@code delta.content} of every chunk, in order. */ + private static String contentOf(List chunks) throws Exception { + StringBuilder content = new StringBuilder(); + for (String chunk : chunks) { + var delta = MAPPER.readTree(chunk).path("choices").path(0).path("delta"); + if (delta.path("content").isTextual()) { + content.append(delta.path("content").asText()); + } + } + return content.toString(); + } + + @Test + void aTextTurnCarryingTheLineSeparatorIsJsonOnEveryPlatform() { + assertEveryChunkIsJson( + "System.lineSeparator()", + ScriptedBackend.textTurn("working" + System.lineSeparator() + TaskLoop.SENTINEL)); + assertEveryChunkIsJson("CRLF", ScriptedBackend.textTurn("working\r\n" + TaskLoop.SENTINEL)); + assertEveryChunkIsJson("LF", ScriptedBackend.textTurn("working\n" + TaskLoop.SENTINEL)); + assertEveryChunkIsJson("bare CR", ScriptedBackend.textTurn("working\r" + TaskLoop.SENTINEL)); + } + + /** + * The default branch of {@code quote}, not the named cases: a control character nobody listed is + * what reopened the gap, so every one of them is checked rather than the four with a short escape. + * Valid JSON is not enough -- any hexadecimal or short escape parses -- so each must also come back + * as itself: a wrong code point, or a CR written with the escape of an LF, parses just as well. + */ + @Test + void everyControlCharacterSurvivesAsJson() throws Exception { + for (int c = 0; c < 0x20; c++) { + String what = "U+" + String.format("%04X", c); + String text = "a" + (char) c + "b"; + List chunks = ScriptedBackend.textTurn(text); + assertEveryChunkIsJson(what, chunks); + assertThat(what, contentOf(chunks), is(text)); + } + String quoted = "say \"hi\"\\ now"; + assertEveryChunkIsJson("quote and backslash", ScriptedBackend.textTurn(quoted)); + assertThat("quote and backslash", contentOf(ScriptedBackend.textTurn(quoted)), is(quoted)); + String beyond = "a\u007fb\u0085c
d"; + assertEveryChunkIsJson("DEL and beyond", ScriptedBackend.textTurn(beyond)); + assertThat("DEL and beyond", contentOf(ScriptedBackend.textTurn(beyond)), is(beyond)); + } + + /** A chunk must also carry the text back unchanged -- escaping may not lose or alter it. */ + @Test + void theTextComesBackOutOfTheChunkUnchanged() throws Exception { + String text = "working" + System.lineSeparator() + "\tdone \"quoted\" \\ U+0001:\u0001"; + assertThat(contentOf(ScriptedBackend.textTurn(text)), is(text)); + } +} diff --git a/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/ServerPropsTest.java b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/ServerPropsTest.java index 169df860c..c6e055033 100644 --- a/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/ServerPropsTest.java +++ b/llama-atmosphere-agent/src/test/java/net/ladenthin/llama/atmosphere/ServerPropsTest.java @@ -16,11 +16,11 @@ class ServerPropsTest { void propsIsTriedNextToTheV1RoutesFirst() { // llama-server serves /props at the root; this project's server serves it under both. assertThat( - ServerProps.candidates("http://127.0.0.1:8080/v1"), - is(List.of("http://127.0.0.1:8080/props", "http://127.0.0.1:8080/v1/props"))); + ServerProps.candidates("http://127.0.0.1:9931/v1"), + is(List.of("http://127.0.0.1:9931/props", "http://127.0.0.1:9931/v1/props"))); assertThat( - ServerProps.candidates("http://127.0.0.1:8080/v1/"), - is(List.of("http://127.0.0.1:8080/props", "http://127.0.0.1:8080/v1/props"))); + ServerProps.candidates("http://127.0.0.1:9931/v1/"), + is(List.of("http://127.0.0.1:9931/props", "http://127.0.0.1:9931/v1/props"))); assertThat(ServerProps.candidates("http://host/api"), is(List.of("http://host/api/props"))); } diff --git a/llama-ffm/README.md b/llama-ffm/README.md index 7d3101e48..3ccd1d148 100644 --- a/llama-ffm/README.md +++ b/llama-ffm/README.md @@ -58,7 +58,7 @@ try (FfmLlamaModel model = FfmLlamaModel.load(new ModelParameters().setModel("mo String metrics = model.metrics(); // The embedded llama-server over the loaded model, WebUI included: - try (FfmNativeServer server = FfmNativeServer.startAttached(model, "--port", "8080")) { + try (FfmNativeServer server = FfmNativeServer.startAttached(model, "--port", "9931")) { // ... serve ... } } @@ -85,10 +85,13 @@ builds like every other module. ```bash mvn -pl llama -am -DskipTests install # the core first -mvn -f llama-ffm/pom.xml verify # 7 model-free tests + the model-backed ones +mvn -f llama-ffm/pom.xml verify # the model-free tests + the model-backed ones # the model-backed test takes the draft model of .github/models.csv, or: mvn -f llama-ffm/pom.xml verify -Dnet.ladenthin.llama.ffm.model=/path/to/model.gguf ``` -The native-backed tests skip themselves without a native build (`llama/src/main/natives`, or -`-Dnet.ladenthin.llama.lib.path=`). +`FfmNativeServerTest` and `FfmTextToSpeechTest` run over fake `Backend` implementations and need +no native library. `JllamaNativeTest` and the model-backed `FfmLlamaModelIntegrationTest` skip +themselves without a native build (`llama/src/main/natives`, or +`-Dnet.ladenthin.llama.lib.path=`); `FfmRpcServerTest` serves the CPU over +loopback and needs one as well. diff --git a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmLlamaModel.java b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmLlamaModel.java index 41c9e8b15..02e417b6e 100644 --- a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmLlamaModel.java +++ b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmLlamaModel.java @@ -146,6 +146,8 @@ public String meta() { * * @param requestJson the OpenAI chat body ({@code {"messages": [...], ...}}) * @return the {@code chat.completion} object + * @throws InvalidRequestException if the server refuses the body, including {@code "n"} greater than 1 + * (one completion per request; send separate requests) */ public String chatCompletions(String requestJson) { return stringOut(JllamaNative.CHAT_COMPLETIONS, requireNonNull(requestJson, "requestJson")); @@ -157,6 +159,8 @@ public String chatCompletions(String requestJson) { * * @param requestJson the OpenAI chat body * @return the stream; close it to release the task early + * @throws InvalidRequestException if the server refuses the body, including {@code "n"} greater than 1 + * (one completion per request; send separate requests) */ public FfmTaskStream chatCompletionsStream(String requestJson) { int task = taskOut(JllamaNative.CHAT_COMPLETIONS_STREAM_REQUEST, requireNonNull(requestJson, "requestJson")); @@ -169,6 +173,8 @@ public FfmTaskStream chatCompletionsStream(String requestJson) { * * @param requestJson the OpenAI chat body * @return the stream of raw result objects + * @throws InvalidRequestException if the server refuses the body, including {@code "n"} greater than 1 + * (one completion per request; send separate requests) */ public FfmTaskStream chatCompletionsRaw(String requestJson) { int task = taskOut(JllamaNative.CHAT_COMPLETIONS_REQUEST, requireNonNull(requestJson, "requestJson")); @@ -180,6 +186,9 @@ public FfmTaskStream chatCompletionsRaw(String requestJson) { * * @param requestJson the request object ({@code {"prompt": ..., ...}}) * @return the result object + * @throws InvalidRequestException if the server refuses the body, including several prompts in one + * request or {@code "n"} greater than 1 (one prompt and one completion per request; send separate + * requests) */ public String completions(String requestJson) { return stringOut(JllamaNative.COMPLETIONS, requireNonNull(requestJson, "requestJson")); @@ -190,16 +199,37 @@ public String completions(String requestJson) { * * @param requestJson the request object * @return the {@code text_completion} object + * @throws InvalidRequestException if the server refuses the body, including several prompts in one + * request or {@code "n"} greater than 1 (one prompt and one completion per request; send separate + * requests) */ public String completionsOai(String requestJson) { return stringOut(JllamaNative.COMPLETIONS_OAI, requireNonNull(requestJson, "requestJson")); } + /** + * A streamed OpenAI {@code /v1/completions} request: each {@link FfmTaskStream#next()} is one + * {@code {"data": , "stop": }} envelope, the last chunk with + * {@code usage} (as {@code LlamaModel.streamCompletionsOai}). + * + * @param requestJson the request object, parsed as {@link #completionsOai(String)} parses it + * @return the stream; close it to release the task early + * @throws InvalidRequestException if the server refuses the body, including several prompts in one + * request or {@code "n"} greater than 1 (one prompt and one completion per request; send separate + * requests) + */ + public FfmTaskStream completionsOaiStream(String requestJson) { + int task = taskOut(JllamaNative.COMPLETIONS_OAI_STREAM_REQUEST, requireNonNull(requestJson, "requestJson")); + return new FfmTaskStream(this, task, JllamaNative.CHAT_COMPLETIONS_STREAM_RECEIVE); + } + /** * A fill-in-the-middle request, blocking (the model needs FIM tokens). * * @param requestJson the request object ({@code input_prefix}, {@code input_suffix}, ...) * @return the result object + * @throws InvalidRequestException if the server refuses the body, including {@code "n"} greater than 1 + * (one completion per request) */ public String infill(String requestJson) { return stringOut(JllamaNative.INFILL, requireNonNull(requestJson, "requestJson")); @@ -210,6 +240,9 @@ public String infill(String requestJson) { * * @param requestJson the request object * @return the stream + * @throws InvalidRequestException if the server refuses the body, including several prompts in one + * request or {@code "n"} greater than 1 (one prompt and one completion per request; send separate + * requests) */ public FfmTaskStream completionsStream(String requestJson) { int task = taskOut(JllamaNative.COMPLETIONS_STREAM_REQUEST, requireNonNull(requestJson, "requestJson")); @@ -343,12 +376,17 @@ public String metrics() { } /** - * A slot save, restore or erase. + * A slot save, restore or erase. As {@code LlamaModel.saveSlot}, the file of a save or restore is a + * file name inside the directory the model was loaded with ({@code --slot-save-path}), not a path. * * @param op the operation * @param slotId the slot - * @param filename the file for {@link SlotOp#SAVE} and {@link SlotOp#RESTORE}; ignored for erase + * @param filename the name of the file inside the slot directory for {@link SlotOp#SAVE} and + * {@link SlotOp#RESTORE}; ignored for erase * @return the server's result object + * @throws InvalidRequestException if {@code filename} has a directory part, contains {@code ..} or + * is empty + * @throws LlamaException if the model was loaded without {@code --slot-save-path}, or the action fails */ public String slotAction(SlotOp op, int slotId, String filename) { requireNonNull(op, "op"); diff --git a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmNativeServer.java b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmNativeServer.java index 21145a196..4351a53ff 100644 --- a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmNativeServer.java +++ b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmNativeServer.java @@ -12,7 +12,6 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; import net.ladenthin.llama.exception.LlamaException; /** @@ -23,11 +22,65 @@ */ public final class FfmNativeServer implements AutoCloseable { + /** The two native calls on a running server's handle; a seam so the locking is testable without it. */ + interface Backend { + + /** + * Whether the worker behind {@code handle} still runs. + * + * @param handle the server handle + * @return {@code true} while it serves + */ + boolean isRunning(MemorySegment handle); + + /** + * Stops the worker, waits for it and frees {@code handle}. + * + * @param handle the server handle + */ + void stop(MemorySegment handle); + } + + /** The real calls into the library. */ + private static final Backend NATIVE = new Backend() { + @Override + public boolean isRunning(MemorySegment handle) { + return (boolean) invoke(JllamaNative.SERVER_IS_RUNNING, handle); + } + + @Override + public void stop(MemorySegment handle) { + JllamaNative.call(JllamaNative.SERVER_STOP, handle); + } + }; + private final MemorySegment handle; - private final AtomicBoolean closed = new AtomicBoolean(); + private final Backend backend; + + /** + * Serializes {@link #isRunning()} and {@link #close()} over {@link #handle}, as in + * {@code NativeServer}: the native stop frees the handle, so a query that is inside its native + * call when another thread closes would read freed memory (shown with this seam: {@code close()} + * returned while {@code isRunning()} was still inside the call). + */ + private final Object lifecycleLock = new Object(); + + /** Set by {@link #close()} under {@link #lifecycleLock}. */ + private boolean closed; private FfmNativeServer(MemorySegment handle) { + this(handle, NATIVE); + } + + /** + * A server over {@code handle} that calls {@code backend}; for tests. + * + * @param handle the server handle + * @param backend the native calls + */ + FfmNativeServer(MemorySegment handle, Backend backend) { this.handle = handle; + this.backend = backend; } /** @@ -88,14 +141,22 @@ public static void setWorkerCommand(String command) { * @return {@code true} while it serves */ public boolean isRunning() { - return !closed.get() && (boolean) invoke(JllamaNative.SERVER_IS_RUNNING, handle); + synchronized (lifecycleLock) { + return !closed && backend.isRunning(handle); + } } - /** Signals shutdown, waits for the worker and releases the handle; a second call is a no-op. */ + /** + * Signals shutdown, waits for the worker and releases the handle; a second call is a no-op. Waits + * for an {@link #isRunning()} call in progress, which must not query a handle being freed. + */ @Override public void close() { - if (closed.compareAndSet(false, true)) { - JllamaNative.call(JllamaNative.SERVER_STOP, handle); + synchronized (lifecycleLock) { + if (!closed) { + closed = true; + backend.stop(handle); + } } } diff --git a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmRpcServer.java b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmRpcServer.java index 7b5e31fe8..baee1dc0f 100644 --- a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmRpcServer.java +++ b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmRpcServer.java @@ -27,7 +27,9 @@ private FfmRpcServer() {} /** * Serves until {@link #stop()}: blocks the calling thread for the life of the server, * or returns at once when the socket cannot be bound (check {@link #isListening()} from another - * thread to tell the two apart, as {@code RpcServer} does). + * thread to tell the two apart, as {@code RpcServer} does). A caller that gives up waiting for the + * server to listen stops it the way {@link #stop()} describes: repeatedly until this call has + * returned, since a stop before the serving thread has entered it is lost. * * @param host the IPv4 literal to bind * @param port the port @@ -45,7 +47,16 @@ public static void serve(String host, int port, int threads, String cacheDir, St } } - /** Stops the running server; a no-op when none runs. */ + /** + * Stops the server and returns once {@link #serve} has returned, also for a stop that arrives before + * the socket is bound. A no-op when no {@link #serve} call is in progress, and it is for the + * {@link #serve} calls in progress when it is made: one started while the stop still waits (a + * restart from another thread) is not stopped. A stop issued before the serving thread has entered + * {@link #serve} is lost, so stopping a server that may not have come up yet means repeating this + * until {@link #serve} has returned. + * + * @throws LlamaException when {@link #serve} has not returned 30 seconds after the request + */ public static void stop() { JllamaNative.call(JllamaNative.RPC_SERVER_STOP); } diff --git a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmTextToSpeech.java b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmTextToSpeech.java index 8d99f3b46..51ac5dbe2 100644 --- a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmTextToSpeech.java +++ b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/FfmTextToSpeech.java @@ -13,7 +13,7 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.util.Objects; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantReadWriteLock; import net.ladenthin.llama.exception.LlamaException; /** @@ -22,11 +22,81 @@ */ public final class FfmTextToSpeech implements AutoCloseable { + /** The two native calls on a loaded pair's handle; a seam so the locking is testable without it. */ + interface Backend { + + /** + * Synthesises {@code text} on {@code handle}. + * + * @param handle the loaded pair + * @param text the text to speak + * @param speakerReferencePath a WAV to clone the voice from, or {@code null} + * @param language the language, or {@code null} + * @param maxFrames the bound on the generated frames + * @param topK the sampling top-k + * @param seed the sampling seed + * @return the WAV bytes + */ + byte[] synthesize(MemorySegment handle, String text, String speakerReferencePath, String language, + int maxFrames, int topK, int seed); + + /** + * Frees {@code handle}, the engine with it. + * + * @param handle the loaded pair + */ + void free(MemorySegment handle); + } + + /** The real calls into the library. */ + private static final Backend NATIVE = new Backend() { + @Override + public byte[] synthesize(MemorySegment handle, String text, String speakerReferencePath, String language, + int maxFrames, int topK, int seed) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment wav = pointerSlot(arena); + MemorySegment length = longSlot(arena); + JllamaNative.call(JllamaNative.TTS_SYNTHESIZE, handle, cString(arena, text), + cString(arena, speakerReferencePath), cString(arena, language), maxFrames, topK, seed, wav, + length); + return takeBytes(wav, length); + } + } + + @Override + public void free(MemorySegment handle) { + invoke(JllamaNative.TTS_FREE, handle); + } + }; + private final MemorySegment handle; - private final AtomicBoolean closed = new AtomicBoolean(); + private final Backend backend; + + /** + * Serializes {@link #close()} against {@link #synthesize}, as in the JNI {@code TextToSpeech} + * (F16): the native free deletes the engine, and nothing on the native side counts the threads + * still inside a call on it, so a free during a synthesis left that synthesis on freed memory. + * Syntheses share the read lock (the engine serializes them itself); close takes the write lock + * and so waits for every synthesis in progress. + */ + private final ReentrantReadWriteLock lifecycleLock = new ReentrantReadWriteLock(); + + /** Set by {@link #close()} under the write lock. */ + private boolean closed; private FfmTextToSpeech(MemorySegment handle) { + this(handle, NATIVE); + } + + /** + * A pair over {@code handle} that calls {@code backend}; for tests. + * + * @param handle the loaded pair + * @param backend the native calls + */ + FfmTextToSpeech(MemorySegment handle, Backend backend) { this.handle = handle; + this.backend = backend; } /** @@ -65,24 +135,28 @@ public static FfmTextToSpeech load(String modelPath, String mmprojPath, int gpuL public byte[] synthesize(String text, String speakerReferencePath, String language, int maxFrames, int topK, int seed) { Objects.requireNonNull(text, "text"); - if (closed.get()) { - throw new IllegalStateException("the model is closed"); - } - try (Arena arena = Arena.ofConfined()) { - MemorySegment wav = pointerSlot(arena); - MemorySegment length = longSlot(arena); - JllamaNative.call(JllamaNative.TTS_SYNTHESIZE, handle, cString(arena, text), - cString(arena, speakerReferencePath), cString(arena, language), maxFrames, topK, seed, wav, - length); - return takeBytes(wav, length); + lifecycleLock.readLock().lock(); + try { + if (closed) { + throw new IllegalStateException("the model is closed"); + } + return backend.synthesize(handle, text, speakerReferencePath, language, maxFrames, topK, seed); + } finally { + lifecycleLock.readLock().unlock(); } } - /** Frees the model pair; a second call is a no-op. */ + /** Frees the model pair once every synthesis in progress has returned; a second call is a no-op. */ @Override public void close() { - if (closed.compareAndSet(false, true)) { - invoke(JllamaNative.TTS_FREE, handle); + lifecycleLock.writeLock().lock(); + try { + if (!closed) { + closed = true; + backend.free(handle); + } + } finally { + lifecycleLock.writeLock().unlock(); } } } diff --git a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/JllamaNative.java b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/JllamaNative.java index 2e2c48f41..a7490f17c 100644 --- a/llama-ffm/src/main/java/net/ladenthin/llama/ffm/JllamaNative.java +++ b/llama-ffm/src/main/java/net/ladenthin/llama/ffm/JllamaNative.java @@ -18,6 +18,7 @@ import java.lang.invoke.MethodHandle; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.function.BiConsumer; import net.ladenthin.llama.LlamaModel; import net.ladenthin.llama.exception.InvalidRequestException; import net.ladenthin.llama.exception.LlamaException; @@ -97,6 +98,8 @@ final class JllamaNative { downcall("jllama_chat_completions_request", json(ADDRESS, ADDRESS, ADDRESS)); static final MethodHandle COMPLETIONS = downcall("jllama_completions", json(ADDRESS, ADDRESS, ADDRESS)); static final MethodHandle COMPLETIONS_OAI = downcall("jllama_completions_oai", json(ADDRESS, ADDRESS, ADDRESS)); + static final MethodHandle COMPLETIONS_OAI_STREAM_REQUEST = + downcall("jllama_completions_oai_stream_request", json(ADDRESS, ADDRESS, ADDRESS)); static final MethodHandle INFILL = downcall("jllama_infill", json(ADDRESS, ADDRESS, ADDRESS)); static final MethodHandle COMPLETIONS_STREAM_REQUEST = downcall("jllama_completions_stream_request", json(ADDRESS, ADDRESS, ADDRESS)); @@ -284,9 +287,26 @@ static byte[] takeBytes(MemorySegment out, MemorySegment length) { /** A {@code char ***} plus {@code size_t *} out-pair, released with {@code jllama_strings_free}. */ static List takeStrings(MemorySegment out, MemorySegment length) { + return takeStrings(out, length, (pointer, count) -> invoke(STRINGS_FREE, pointer, count)); + } + + /** + * {@link #takeStrings(MemorySegment, MemorySegment)} with the free function passed in, for tests. + * + *

    A non-NULL array is freed even when it is empty: {@code jllama_rpc_server_devices} allocates + * one for zero entries too, and returning early for {@code count == 0} leaked it (measured: the + * free function was never called). + * + * @param out the out-parameter holding the array pointer + * @param length the out-parameter holding the element count + * @param free frees the array with its element count + * @return the strings, in order + */ + static List takeStrings( + MemorySegment out, MemorySegment length, BiConsumer free) { MemorySegment pointer = out.get(ADDRESS, 0); long count = length.get(JAVA_LONG, 0); - if (pointer.equals(MemorySegment.NULL) || count == 0) { + if (pointer.equals(MemorySegment.NULL)) { return List.of(); } try { @@ -297,7 +317,7 @@ static List takeStrings(MemorySegment out, MemorySegment length) { } return List.of(strings); } finally { - invoke(STRINGS_FREE, pointer, count); + free.accept(pointer, count); } } diff --git a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmLlamaModelIntegrationTest.java b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmLlamaModelIntegrationTest.java index dacb66b50..7b67ce700 100644 --- a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmLlamaModelIntegrationTest.java +++ b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmLlamaModelIntegrationTest.java @@ -18,7 +18,9 @@ import java.net.HttpURLConnection; import java.net.ServerSocket; import java.net.URI; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import net.ladenthin.llama.exception.InvalidRequestException; import net.ladenthin.llama.exception.LlamaException; @@ -26,6 +28,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; /** * A model through the C API: loaded with the core's {@code ModelParameters}, the contract of each @@ -122,6 +125,22 @@ void rawCompletionAndItsStream() throws IOException { assertThat(results, greaterThan(0)); } + @Test + void oaiCompletionStreamEndsWithUsage() throws IOException { + JsonNode last = null; + try (FfmTaskStream stream = + model.completionsOaiStream("{\"prompt\": \"Hello\", \"max_tokens\": 3, \"temperature\": 0}")) { + while (stream.hasNext()) { + JsonNode data = JSON.readTree(stream.next()).path("data"); + for (JsonNode chunk : data.isArray() ? data : JSON.createArrayNode().add(data)) { + assertEquals("text_completion", chunk.path("object").asText()); + last = chunk; + } + } + } + assertTrue(last != null && last.path("usage").path("completion_tokens").asInt() > 0, String.valueOf(last)); + } + @Test void tokenizeAndDetokenizeRoundTrip() throws IOException { int[] tokens = model.tokenize("Hello", false); @@ -166,10 +185,38 @@ void loraAdaptersAndConfigure() throws IOException { } @Test - void slotEraseAnswersAndASaveWithoutAFilenameIsRefused() throws IOException { + void slotEraseAnswersAndASaveWithoutASlotDirectoryIsRefused() throws IOException { assertTrue(JSON.readTree(model.slotAction(SlotOp.ERASE, 0, null)).isObject()); - LlamaException e = assertThrows(LlamaException.class, () -> model.slotAction(SlotOp.SAVE, 0, null)); - assertEquals("Filename is required for slot save", e.getMessage()); + LlamaException e = assertThrows(LlamaException.class, () -> model.slotAction(SlotOp.SAVE, 0, "slot0.bin")); + assertFalse(e instanceof InvalidRequestException, e.getMessage()); + assertEquals( + "Saving or restoring a slot needs a slot directory: load the model with --slot-save-path

    " + + "(ModelParameters.setSlotSavePath)", + e.getMessage()); + } + + @Test + void aSlotFileIsANameInsideTheSlotDirectory(@TempDir Path slotDir) throws IOException { + ModelParameters parameters = new ModelParameters() + .setModel(FfmTestSupport.assumeModel().toString()) + .setCtxSize(128) + .setGpuLayers(0) + .setThreads(2) + .setSlotSavePath(slotDir.toString()); + try (FfmLlamaModel withSlots = FfmLlamaModel.load(parameters, null)) { + String saved = withSlots.slotAction(SlotOp.SAVE, 0, "slot0.bin"); + assertTrue(Files.exists(slotDir.resolve("slot0.bin")), saved); + assertEquals("slot0.bin", JSON.readTree(saved).path("filename").asText(), saved); + assertEquals(0, JSON.readTree(withSlots.slotAction(SlotOp.RESTORE, 0, "slot0.bin")).path("id_slot").asInt()); + // unique to the run, so a file another run left next to the temp directory cannot decide it + String escapedName = "escaped-" + UUID.randomUUID() + ".bin"; + assertThrows( + InvalidRequestException.class, () -> withSlots.slotAction(SlotOp.SAVE, 0, "../" + escapedName)); + assertThrows( + InvalidRequestException.class, + () -> withSlots.slotAction(SlotOp.RESTORE, 0, slotDir.resolve("slot0.bin").toString())); + assertFalse(Files.exists(slotDir.getParent().resolve(escapedName))); + } } @Test diff --git a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmNativeServerTest.java b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmNativeServerTest.java new file mode 100644 index 000000000..20e636197 --- /dev/null +++ b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmNativeServerTest.java @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: 2026 Bernard Ladenthin +// +// SPDX-License-Identifier: MIT +package net.ladenthin.llama.ffm; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; + +import java.lang.foreign.MemorySegment; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +/** + * The lifecycle locking of {@link FfmNativeServer}, over a fake backend: no library, no model. + * + *

    The native stop frees the handle. Without the lock, {@code close()} freed it while an + * {@code isRunning()} on another thread was still inside its native call on it (measured with this + * seam on JDK 25: {@code close()} returned while {@code isRunning()} had not); {@code NativeServer} + * has serialized the two all along. + */ +class FfmNativeServerTest { + + /** Upper bound for the waits below; reaching it is a failure, not a slow machine. */ + private static final long AWAIT_SECONDS = 10; + + /** How long {@code close()} is given to show that it does not wait. */ + private static final long CLOSE_GRACE_MILLIS = 500; + + @Test + void closeWaitsForAnIsRunningCallInsideTheNativeCall() throws Exception { + List events = new CopyOnWriteArrayList<>(); + CountDownLatch insideIsRunning = new CountDownLatch(1); + CountDownLatch releaseIsRunning = new CountDownLatch(1); + FfmNativeServer server = new FfmNativeServer(MemorySegment.NULL, new FfmNativeServer.Backend() { + @Override + public boolean isRunning(MemorySegment handle) { + events.add("isRunning entered"); + insideIsRunning.countDown(); + try { + releaseIsRunning.await(AWAIT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + events.add("isRunning returned"); + return true; + } + + @Override + public void stop(MemorySegment handle) { + events.add("stop (frees the handle)"); + } + }); + + Thread reader = new Thread(server::isRunning, "isRunning"); + reader.start(); + assertThat(insideIsRunning.await(AWAIT_SECONDS, TimeUnit.SECONDS), is(true)); + Thread closer = new Thread(server::close, "close"); + closer.start(); + closer.join(CLOSE_GRACE_MILLIS); + boolean closeWaited = closer.isAlive(); + releaseIsRunning.countDown(); + reader.join(TimeUnit.SECONDS.toMillis(AWAIT_SECONDS)); + closer.join(TimeUnit.SECONDS.toMillis(AWAIT_SECONDS)); + + assertThat(closeWaited, is(true)); + assertThat(events, contains("isRunning entered", "isRunning returned", "stop (frees the handle)")); + } + + @Test + void isRunningAfterCloseDoesNotQueryTheHandle() { + List events = new CopyOnWriteArrayList<>(); + FfmNativeServer server = new FfmNativeServer(MemorySegment.NULL, recordingBackend(events)); + + server.close(); + + assertThat(server.isRunning(), is(false)); + assertThat(events, contains("stop")); + } + + @Test + void secondCloseIsANoOp() { + List events = new CopyOnWriteArrayList<>(); + FfmNativeServer server = new FfmNativeServer(MemorySegment.NULL, recordingBackend(events)); + + server.close(); + server.close(); + + assertThat(events, contains("stop")); + } + + @Test + void isRunningAsksTheBackendWhileOpen() { + List events = new CopyOnWriteArrayList<>(); + FfmNativeServer server = new FfmNativeServer(MemorySegment.NULL, recordingBackend(events)); + + assertThat(server.isRunning(), is(true)); + assertThat(events, contains("isRunning")); + } + + private static FfmNativeServer.Backend recordingBackend(List events) { + return new FfmNativeServer.Backend() { + @Override + public boolean isRunning(MemorySegment handle) { + events.add("isRunning"); + return true; + } + + @Override + public void stop(MemorySegment handle) { + events.add("stop"); + } + }; + } +} diff --git a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmRpcServerTest.java b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmRpcServerTest.java new file mode 100644 index 000000000..fd8ef0338 --- /dev/null +++ b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmRpcServerTest.java @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2026 Bernard Ladenthin +// +// SPDX-License-Identifier: MIT +package net.ladenthin.llama.ffm; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * {@link FfmRpcServer} over loopback, serving the CPU: no model. The stop-before-bind case itself + * needs a seam inside the serve call and is pinned in {@code test_c_api.cpp}; this pins the FFM half of + * the contract around it. + */ +class FfmRpcServerTest { + + /** Upper bound for the waits below; reaching it is a failure, not a slow machine. */ + private static final long AWAIT_MILLIS = 10_000; + + /** Ports are tried from a range, so parallel jobs on one runner do not collide. */ + private static final int ATTEMPTS = 50; + + @BeforeAll + static void nativesLoadable() { + FfmTestSupport.assumeNativesLoadable(); + } + + @Test + void aStopWhileNoServeRunsDoesNotStopTheNextServeAndAStopEndsIt() throws Exception { + FfmRpcServer.stop(); // nothing runs: a no-op that must not carry over + + boolean served = false; + for (int attempt = 0; attempt < ATTEMPTS && !served; attempt++) { + int port = 42_000 + (int) ((System.nanoTime() / 1000 + attempt * 97L) % 20_000); + AtomicReference failure = new AtomicReference<>(); + Thread serve = new Thread( + () -> { + try { + FfmRpcServer.serve("127.0.0.1", port, 2, null, "CPU"); + } catch (Throwable t) { + failure.set(t); + } + }, + "ffm-rpc-serve-" + port); + serve.setDaemon(true); + serve.start(); + long deadline = System.nanoTime() + AWAIT_MILLIS * 1_000_000L; + while (!FfmRpcServer.isListening() && serve.isAlive() && System.nanoTime() < deadline) { + serve.join(10); + } + if (FfmRpcServer.isListening()) { + served = true; + FfmRpcServer.stop(); + serve.join(AWAIT_MILLIS); + assertThat("serve returned after the stop", serve.isAlive(), is(false)); + assertThat(failure.get(), is(nullValue())); + } else { + serve.join(AWAIT_MILLIS); // the port was taken: serve returned at once + } + } + assertThat("listened on a free port of the range", served, is(true)); + assertThat(FfmRpcServer.isListening(), is(false)); + } +} diff --git a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTextToSpeechTest.java b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTextToSpeechTest.java new file mode 100644 index 000000000..02fa1f95d --- /dev/null +++ b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/FfmTextToSpeechTest.java @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2026 Bernard Ladenthin +// +// SPDX-License-Identifier: MIT +package net.ladenthin.llama.ffm; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.lang.foreign.MemorySegment; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +/** + * The lifecycle locking of {@link FfmTextToSpeech}, over a fake backend: no library, no model. + * + *

    The native free deletes the engine (and the mutex a synthesis holds), and nothing counts users on + * the native side. Without a lock, {@code close()} freed it while a {@code synthesize()} on another + * thread was still inside its native call on it — the use-after-free F16 fixed for the JNI + * {@code TextToSpeech}. + */ +class FfmTextToSpeechTest { + + /** Upper bound for the waits below; reaching it is a failure, not a slow machine. */ + private static final long AWAIT_SECONDS = 10; + + /** How long {@code close()} is given to show that it does not wait. */ + private static final long CLOSE_GRACE_MILLIS = 500; + + private static final byte[] WAV = {1, 2, 3}; + + /** A backend that records the calls and holds a synthesis until it is released. */ + private static final class GatedBackend implements FfmTextToSpeech.Backend { + + final List events = new CopyOnWriteArrayList<>(); + final CountDownLatch insideSynthesize = new CountDownLatch(1); + final CountDownLatch releaseSynthesize = new CountDownLatch(1); + + @Override + public byte[] synthesize(MemorySegment handle, String text, String speakerReferencePath, String language, + int maxFrames, int topK, int seed) { + events.add("synthesize entered"); + insideSynthesize.countDown(); + try { + releaseSynthesize.await(AWAIT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + events.add("synthesize returned"); + return WAV; + } + + @Override + public void free(MemorySegment handle) { + events.add("free (deletes the engine)"); + } + } + + @Test + void closeWaitsForASynthesisInsideTheNativeCall() throws Exception { + GatedBackend backend = new GatedBackend(); + FfmTextToSpeech tts = new FfmTextToSpeech(MemorySegment.NULL, backend); + + Thread speaker = new Thread(() -> tts.synthesize("hi", null, null, 10, 1, 1), "synthesize"); + speaker.start(); + assertThat(backend.insideSynthesize.await(AWAIT_SECONDS, TimeUnit.SECONDS), is(true)); + Thread closer = new Thread(tts::close, "close"); + closer.start(); + closer.join(CLOSE_GRACE_MILLIS); + boolean closeWaited = closer.isAlive(); + backend.releaseSynthesize.countDown(); + speaker.join(TimeUnit.SECONDS.toMillis(AWAIT_SECONDS)); + closer.join(TimeUnit.SECONDS.toMillis(AWAIT_SECONDS)); + + assertThat(closeWaited, is(true)); + assertThat( + backend.events, + contains("synthesize entered", "synthesize returned", "free (deletes the engine)")); + } + + @Test + void synthesizeAfterCloseIsRefusedWithoutTouchingTheHandle() { + GatedBackend backend = new GatedBackend(); + backend.releaseSynthesize.countDown(); + FfmTextToSpeech tts = new FfmTextToSpeech(MemorySegment.NULL, backend); + + tts.close(); + + assertThrows(IllegalStateException.class, () -> tts.synthesize("hi", null, null, 10, 1, 1)); + assertThat(backend.events, contains("free (deletes the engine)")); + } + + @Test + void closeTwiceFreesOnce() { + GatedBackend backend = new GatedBackend(); + FfmTextToSpeech tts = new FfmTextToSpeech(MemorySegment.NULL, backend); + + tts.close(); + tts.close(); + + assertThat(backend.events, contains("free (deletes the engine)")); + } + + @Test + void synthesizeReturnsTheBackendsWav() { + GatedBackend backend = new GatedBackend(); + backend.releaseSynthesize.countDown(); + FfmTextToSpeech tts = new FfmTextToSpeech(MemorySegment.NULL, backend); + + assertThat(tts.synthesize("hi", null, null, 10, 1, 1), is(WAV)); + } + + @Test + void synthesizeWithoutTextIsRefused() { + FfmTextToSpeech tts = new FfmTextToSpeech(MemorySegment.NULL, new GatedBackend()); + + assertThrows(NullPointerException.class, () -> tts.synthesize(null, null, null, 10, 1, 1)); + } +} diff --git a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/JllamaNativeTest.java b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/JllamaNativeTest.java index 5ce1fa92b..1c80d89ce 100644 --- a/llama-ffm/src/test/java/net/ladenthin/llama/ffm/JllamaNativeTest.java +++ b/llama-ffm/src/test/java/net/ladenthin/llama/ffm/JllamaNativeTest.java @@ -11,8 +11,14 @@ import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static java.lang.foreign.ValueLayout.ADDRESS; +import static java.lang.foreign.ValueLayout.JAVA_LONG; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import net.ladenthin.llama.exception.LlamaException; @@ -33,6 +39,70 @@ static void nativesLoadable() { FfmTestSupport.assumeNativesLoadable(); } + /** + * {@code jllama_rpc_server_devices} allocates an array for zero entries too; returning early for + * {@code count == 0} never freed it (measured: a deliberately invalid pointer that glibc's + * {@code free()} aborts on passed through without a call). + */ + @Test + void takeStringsFreesANonNullArrayEvenWhenItIsEmpty() { + try (Arena arena = Arena.ofConfined()) { + MemorySegment array = arena.allocate(ADDRESS); + MemorySegment out = JllamaNative.pointerSlot(arena); + out.set(ADDRESS, 0, array); + MemorySegment length = arena.allocate(JAVA_LONG); + length.set(JAVA_LONG, 0, 0L); + List freed = new ArrayList<>(); + + List strings = JllamaNative.takeStrings( + out, length, (pointer, count) -> freed.add(pointer.address() + "/" + count)); + + assertThat(strings, is(List.of())); + assertThat(freed, is(List.of(array.address() + "/0"))); + } + } + + /** + * A populated array is read entry by entry in order and freed with its element count: a wrong + * count only leaks the element strings inside {@code jllama_strings_free}, which the JVM cannot + * see, and the one native caller with a test returns a single entry, which hides the order. + */ + @Test + void takeStringsReadsEveryEntryInOrderAndFreesTheArrayWithItsCount() { + try (Arena arena = Arena.ofConfined()) { + MemorySegment array = arena.allocate(ADDRESS, 2); + array.setAtIndex(ADDRESS, 0, arena.allocateFrom("alpha", StandardCharsets.UTF_8)); + array.setAtIndex(ADDRESS, 1, arena.allocateFrom("beta", StandardCharsets.UTF_8)); + MemorySegment out = JllamaNative.pointerSlot(arena); + out.set(ADDRESS, 0, array); + MemorySegment length = arena.allocate(JAVA_LONG); + length.set(JAVA_LONG, 0, 2L); + List freed = new ArrayList<>(); + + List strings = JllamaNative.takeStrings( + out, length, (pointer, count) -> freed.add(pointer.address() + "/" + count)); + + assertThat(strings, is(List.of("alpha", "beta"))); + assertThat(freed, is(List.of(array.address() + "/2"))); + } + } + + @Test + void takeStringsDoesNotFreeANullArray() { + try (Arena arena = Arena.ofConfined()) { + MemorySegment out = JllamaNative.pointerSlot(arena); + out.set(ADDRESS, 0, MemorySegment.NULL); + MemorySegment length = arena.allocate(JAVA_LONG); + length.set(JAVA_LONG, 0, 0L); + List freed = new ArrayList<>(); + + List strings = JllamaNative.takeStrings(out, length, (pointer, count) -> freed.add("freed")); + + assertThat(strings, is(List.of())); + assertThat(freed, is(List.of())); + } + } + @Test void apiVersionIsTheVersionThisModuleWasBuiltAs() { String expected = System.getProperty("net.ladenthin.llama.ffm.expected.version"); diff --git a/llama-langchain4j/README.md b/llama-langchain4j/README.md index 6b9f55d5e..29216a342 100644 --- a/llama-langchain4j/README.md +++ b/llama-langchain4j/README.md @@ -128,8 +128,11 @@ jina-reranker models the core test jobs already download) — the `presencePenalty`, `stopSequences`. - **Streaming tool calls + thinking events.** `JllamaStreamingChatModel` streams over the - native OpenAI chunk path: `delta.tool_calls` fragments surface as `onPartialToolCall` / - `onCompleteToolCall` events and land on the final response as + native OpenAI chunk path: the arguments of a `delta.tool_calls` call surface as + `onPartialToolCall` once the call has a name (arguments that arrived before it are reported then, + so the partial calls add up to the complete arguments), every assembled call as + `onCompleteToolCall` as soon as the next call starts or the stream ends, and the calls + land on the final response as `AiMessage.toolExecutionRequests()` (finish reason `TOOL_EXECUTION`); reasoning deltas surface as `onPartialThinking` and as `AiMessage.thinking()`; both the blocking and the streamed final response carry the model's real finish reason and token usage. diff --git a/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java b/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java index 550a6c2c3..e05478933 100644 --- a/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java +++ b/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java @@ -31,6 +31,7 @@ import java.util.Base64; import java.util.List; import java.util.Locale; +import java.util.StringJoiner; import net.ladenthin.llama.json.RerankResponseParser; import net.ladenthin.llama.parameters.InferenceParameters; import net.ladenthin.llama.value.ContentPart; @@ -56,6 +57,13 @@ final class LangChain4jMapping { /** OAI {@code response_format} payload selecting plain JSON-object mode (no schema). */ static final String JSON_OBJECT_RESPONSE_FORMAT = "{\"type\":\"json_object\"}"; + /** OAI {@code stream_options} payload asking the native stream for its trailing usage chunk. */ + static final String INCLUDE_USAGE_STREAM_OPTIONS = "{\"include_usage\":true}"; + + /** Reads the reasoning of a blocking answer from its raw JSON. */ + private static final net.ladenthin.llama.json.ChatResponseParser CHAT_RESPONSE_PARSER = + new net.ladenthin.llama.json.ChatResponseParser(); + private LangChain4jMapping() {} /** @@ -92,8 +100,11 @@ static net.ladenthin.llama.parameters.ChatRequest toJllamaRequest(ChatRequest re */ static InferenceParameters toStreamingParameters(ChatRequest request) { net.ladenthin.llama.parameters.ChatRequest jllama = toJllamaRequest(request); - InferenceParameters params = - InferenceParameters.empty().withMessagesJson(jllama.buildMessagesJson()); + // The native stream carries a usage chunk only when asked for; without it the streamed + // final response would lack the token usage the blocking one has. + InferenceParameters params = InferenceParameters.empty() + .withMessagesJson(jllama.buildMessagesJson()) + .withStreamOptions(INCLUDE_USAGE_STREAM_OPTIONS); java.util.Optional toolsJson = jllama.buildToolsJson(); if (toolsJson.isPresent()) { params = params.withToolsJson(toolsJson.get()); @@ -202,8 +213,17 @@ static String toToolChoiceString(ToolChoice choice) { private static AiMessage toAiMessage(net.ladenthin.llama.value.ChatResponse response) { net.ladenthin.llama.value.ChatMessage first = response.getFirstMessage().orElse(null); + // The reasoning is not part of the typed message; read it from the native answer, as the + // streamed path does from the reasoning_content deltas. + String thinking = CHAT_RESPONSE_PARSER.extractChoiceReasoningContent(response.getRawJson()); if (first == null || first.getToolCalls().isEmpty()) { - return AiMessage.from(response.getFirstContent()); + if (thinking.isEmpty()) { + return AiMessage.from(response.getFirstContent()); + } + return AiMessage.builder() + .text(response.getFirstContent()) + .thinking(thinking) + .build(); } List requests = new ArrayList<>(first.getToolCalls().size()); for (ToolCall call : first.getToolCalls()) { @@ -218,6 +238,9 @@ private static AiMessage toAiMessage(net.ladenthin.llama.value.ChatResponse resp if (text != null && !text.isEmpty()) { builder.text(text); } + if (!thinking.isEmpty()) { + builder.thinking(thinking); + } return builder.build(); } @@ -259,10 +282,11 @@ private static net.ladenthin.llama.value.ChatMessage toJllamaUserMessage(UserMes return new net.ladenthin.llama.value.ChatMessage("user", message.singleText()); } if (!hasNonTextContent(message)) { - // Multiple text parts, no media: keep the flat-string form (no array-content needed). - StringBuilder text = new StringBuilder(); + // Multiple text parts, no media: keep the flat-string form (no array-content needed), + // joined with a newline as upstream's concat_content_parts does. + StringJoiner text = new StringJoiner("\n"); for (Content content : message.contents()) { - text.append(((TextContent) content).text()); + text.add(((TextContent) content).text()); } return new net.ladenthin.llama.value.ChatMessage("user", text.toString()); } diff --git a/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/StreamingChunkAssembler.java b/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/StreamingChunkAssembler.java index b224a28f2..a93e3f606 100644 --- a/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/StreamingChunkAssembler.java +++ b/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/StreamingChunkAssembler.java @@ -31,9 +31,16 @@ *

  • {@code delta.reasoning_content} → * {@link StreamingChatResponseHandler#onPartialThinking(PartialThinking)} *
  • {@code delta.tool_calls[*]} fragments → accumulated per OpenAI {@code index} (id and - * name arrive on the first fragment, arguments split across fragments), each forwarded as - * {@link StreamingChatResponseHandler#onPartialToolCall(PartialToolCall)} and completed as - * {@link StreamingChatResponseHandler#onCompleteToolCall(CompleteToolCall)} + * name usually arrive on the first fragment, arguments split across fragments). The argument + * text is forwarded as {@link StreamingChatResponseHandler#onPartialToolCall(PartialToolCall)} + * once its call has a name ({@code PartialToolCall} refuses empty arguments and a blank name): + * an id/name-only header is reported with the first argument fragment, and arguments that + * arrive before the name are reported together with the fragment that brings the name, so the + * partials of a call always make up its complete arguments. Every call is + * completed as {@link StreamingChatResponseHandler#onCompleteToolCall(CompleteToolCall)} + * as soon as a call with a higher index starts (llama.cpp only ever extends the last call of + * a message), the last one, and any call that had no name yet at that point, by + * {@link #complete()} *
  • {@code finish_reason} / trailing {@code usage} chunk → finish reason and * {@link TokenUsage} on the final response *
@@ -51,12 +58,25 @@ private static final class PartialTool { String id = ""; String name = ""; final StringBuilder arguments = new StringBuilder(); + /** How many characters of {@link #arguments} were already reported as partials. */ + int reportedLength; + /** Whether {@code onCompleteToolCall} was already sent for this call. */ + boolean completed; + + ToolExecutionRequest toRequest() { + return ToolExecutionRequest.builder() + .id(id) + .name(name) + .arguments(arguments.toString()) + .build(); + } } private final StreamingChatResponseHandler handler; private final StringBuilder text = new StringBuilder(); private final StringBuilder thinking = new StringBuilder(); private final Map toolsByIndex = new TreeMap<>(); + private int highestIndex = -1; private String finishReason = ""; private TokenUsage tokenUsage; @@ -114,33 +134,80 @@ void accept(String chunkJson) { private void acceptToolCallFragment(JsonNode fragment) { int index = fragment.path("index").asInt(0); + JsonNode function = fragment.path("function"); + String idFragment = fragment.path("id").isTextual() ? fragment.path("id").asText() : ""; + String nameFragment = function.path("name").isTextual() ? function.path("name").asText() : ""; + String argumentsFragment = + function.path("arguments").isTextual() ? function.path("arguments").asText() : ""; PartialTool tool = toolsByIndex.get(index); + if (tool != null && tool.completed) { + // The handler already holds this call's CompleteToolCall; merging a change would make the + // final request differ from it. + if (!argumentsFragment.isEmpty() + || (!nameFragment.isEmpty() && !nameFragment.equals(tool.name)) + || (!idFragment.isEmpty() && !idFragment.equals(tool.id))) { + throw new IllegalStateException("A delta changes tool call " + index + + ", which was already reported complete (the highest tool call index seen is " + + highestIndex + "): " + fragment); + } + return; + } + if (index > highestIndex) { + completeCallsBelow(index); + highestIndex = index; + } if (tool == null) { tool = new PartialTool(); toolsByIndex.put(index, tool); } - if (fragment.path("id").isTextual() && !fragment.path("id").asText().isEmpty()) { - tool.id = fragment.path("id").asText(); + if (!idFragment.isEmpty()) { + tool.id = idFragment; } - JsonNode function = fragment.path("function"); - if (function.path("name").isTextual() && !function.path("name").asText().isEmpty()) { - tool.name = function.path("name").asText(); + if (!nameFragment.isEmpty()) { + tool.name = nameFragment; } - String argumentsFragment = - function.path("arguments").isTextual() ? function.path("arguments").asText() : ""; tool.arguments.append(argumentsFragment); + // PartialToolCall refuses empty arguments and a blank name, and llama.cpp sends both: the + // first delta of a call usually carries id and name only, and its incremental parser may + // report a call before its name. Such a delta only feeds the assembled call; what it held + // back is reported with the first delta that can be, so no argument text is lost. + if (tool.name.isEmpty() || tool.arguments.length() == tool.reportedLength) { + return; + } + String unreported = tool.arguments.substring(tool.reportedLength); + tool.reportedLength = tool.arguments.length(); handler.onPartialToolCall(PartialToolCall.builder() .index(index) .id(tool.id) .name(tool.name) - .partialArguments(argumentsFragment) + .partialArguments(unreported) .build()); } /** - * Finalize the stream: emit {@code onCompleteToolCall} for every accumulated call and - * build the final {@link ChatResponse} (text and/or tool execution requests, thinking, - * finish reason, token usage when the stream carried a usage chunk). + * Completes every call below {@code index} that is still open and has a name, in index order: + * llama.cpp's {@code compute_diffs} only ever extends the last tool call of a message, so a call + * is final once one with a higher index starts. A call without a name stays open for + * {@link #complete()}, so that no {@link CompleteToolCall} with a blank name is sent. + */ + private void completeCallsBelow(int index) { + for (Map.Entry entry : toolsByIndex.entrySet()) { + if (entry.getKey() >= index) { + break; + } + PartialTool tool = entry.getValue(); + if (!tool.completed && !tool.name.isEmpty()) { + tool.completed = true; + handler.onCompleteToolCall(new CompleteToolCall(entry.getKey(), tool.toRequest())); + } + } + } + + /** + * Finalize the stream: emit {@code onCompleteToolCall} for every accumulated call not yet + * reported complete, in index order, and build the final {@link ChatResponse} (text and/or + * tool execution requests, thinking, finish reason, token usage when the stream carried a + * usage chunk). * * @return the assembled final response for {@code onCompleteResponse} */ @@ -156,12 +223,11 @@ ChatResponse complete() { List requests = new ArrayList<>(toolsByIndex.size()); for (Map.Entry entry : toolsByIndex.entrySet()) { PartialTool tool = entry.getValue(); - ToolExecutionRequest request = ToolExecutionRequest.builder() - .id(tool.id) - .name(tool.name) - .arguments(tool.arguments.toString()) - .build(); - handler.onCompleteToolCall(new CompleteToolCall(entry.getKey(), request)); + ToolExecutionRequest request = tool.toRequest(); + if (!tool.completed) { + tool.completed = true; + handler.onCompleteToolCall(new CompleteToolCall(entry.getKey(), request)); + } requests.add(request); } message.toolExecutionRequests(requests); diff --git a/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/LangChain4jMappingTest.java b/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/LangChain4jMappingTest.java index 320dd3275..24e2e3d72 100644 --- a/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/LangChain4jMappingTest.java +++ b/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/LangChain4jMappingTest.java @@ -77,11 +77,39 @@ void flattensMultiTextUserMessageToText() { ChatMessage mapped = LangChain4jMapping.toJllamaRequest(request).getMessages().get(0); assertThat(mapped.getRole(), is("user")); - assertThat(mapped.getContent(), is("Hello world")); + assertThat(mapped.getContent(), is("Hello \nworld")); // Text-only content needs no multimodal array form. assertThat(mapped.hasParts(), is(false)); } + @Test + void joinsTextPartsWithANewlineLikeUpstream() { + ChatRequest request = + ChatRequest.builder() + .messages(UserMessage.from(TextContent.from("first"), TextContent.from("second"))) + .build(); + + ChatMessage mapped = LangChain4jMapping.toJllamaRequest(request).getMessages().get(0); + + // Upstream's concat_content_parts joins text parts with a newline; without a separator the + // words at the seam merge ("firstsecond"). + assertThat(mapped.getContent(), is("first\nsecond")); + } + + @Test + void streamingParametersRequestTheUsageChunk() throws Exception { + ChatRequest request = + ChatRequest.builder().messages(UserMessage.from("hi")).build(); + + String json = LangChain4jMapping.toStreamingParameters(request).toJson(); + + // Upstream emits the trailing usage chunk only for stream_options.include_usage; without it + // the streamed response never carries token usage, unlike the blocking one. + com.fasterxml.jackson.databind.JsonNode node = + new com.fasterxml.jackson.databind.ObjectMapper().readTree(json); + assertThat(node.path("stream_options").path("include_usage").asBoolean(false), is(true)); + } + @Test void appliesSamplingParametersToInferenceJson() { ChatRequest request = @@ -263,6 +291,21 @@ void mapsAssistantToolCallTurnIntoHistory() { assertThat(result.getToolCallId().orElse(""), is("call_7")); } + @Test + void blockingResponseCarriesTheThinkingLikeTheStreamedOne() { + // The streamed final response sets AiMessage.thinking() from reasoning_content; the blocking + // one must carry the same reasoning, which the native answer holds in the raw JSON. + net.ladenthin.llama.value.ChatResponse response = new net.ladenthin.llama.json.ChatResponseParser() + .parseResponse("{\"id\":\"id-1\",\"choices\":[{\"index\":0,\"finish_reason\":\"stop\"," + + "\"message\":{\"role\":\"assistant\",\"content\":\"42\",\"reasoning_content\":\"6 times 7\"}}]}"); + + dev.langchain4j.model.chat.response.ChatResponse mapped = + LangChain4jMapping.toLangChainResponse(response); + + assertThat(mapped.aiMessage().text(), is("42")); + assertThat(mapped.aiMessage().thinking(), is("6 times 7")); + } + @Test void toolCallResponseBecomesAiMessageWithToolExecutionRequests() { ChatMessage assistant = ChatMessage.assistantToolCalls( diff --git a/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/StreamingChunkAssemblerTest.java b/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/StreamingChunkAssemblerTest.java index 171393316..6c06b060e 100644 --- a/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/StreamingChunkAssemblerTest.java +++ b/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/StreamingChunkAssemblerTest.java @@ -4,13 +4,19 @@ package net.ladenthin.llama.langchain4j; +import static java.util.stream.Collectors.joining; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; +import dev.langchain4j.agent.tool.ToolExecutionRequest; import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.model.chat.response.CompleteToolCall; import dev.langchain4j.model.chat.response.PartialThinking; @@ -34,6 +40,8 @@ private static final class RecordingHandler implements StreamingChatResponseHand final List thinking = new ArrayList<>(); final List partialToolCalls = new ArrayList<>(); final List completeToolCalls = new ArrayList<>(); + /** Every tool-call event in arrival order, e.g. {@code "partial:0"}, {@code "complete:0"}. */ + final List toolCallEvents = new ArrayList<>(); @Override public void onPartialResponse(String partialResponse) { @@ -48,11 +56,13 @@ public void onPartialThinking(PartialThinking partialThinking) { @Override public void onPartialToolCall(PartialToolCall partialToolCall) { partialToolCalls.add(partialToolCall); + toolCallEvents.add("partial:" + partialToolCall.index()); } @Override public void onCompleteToolCall(CompleteToolCall completeToolCall) { completeToolCalls.add(completeToolCall); + toolCallEvents.add("complete:" + completeToolCall.index()); } @Override @@ -71,6 +81,21 @@ private static String contentChunk(String content) { + "\"delta\":{\"content\":\"" + content + "\"},\"finish_reason\":null}]}"; } + /** + * langchain4j's contract for the partial events of one call: concatenated, the argument fragments + * are the complete call's arguments, and each carries the complete call's id and name. + */ + private static void assertPartialsMakeUpTheCompleteCall(RecordingHandler handler) { + ToolExecutionRequest complete = handler.completeToolCalls.get(0).toolExecutionRequest(); + assertThat( + handler.partialToolCalls.stream().map(PartialToolCall::partialArguments).collect(joining()), + is(complete.arguments())); + for (PartialToolCall partial : handler.partialToolCalls) { + assertThat(partial.id(), is(complete.id())); + assertThat(partial.name(), is(complete.name())); + } + } + @Test void assemblesPlainTextStream() { RecordingHandler handler = new RecordingHandler(); @@ -118,6 +143,60 @@ void assemblesStreamedToolCallAcrossFragments() { assertThat(response.tokenUsage().outputTokenCount(), is(5)); } + /** + * llama.cpp's first delta of a tool call usually carries the id and the name only: its diff + * writer leaves {@code arguments} out while they are still empty. langchain4j's + * {@link PartialToolCall} refuses empty partial arguments, so that delta must not become one. + */ + @Test + void toolCallDeltaWithoutArguments_doesNotFailTheStream() { + RecordingHandler handler = new RecordingHandler(); + StreamingChunkAssembler assembler = new StreamingChunkAssembler(handler); + + assertDoesNotThrow(() -> assembler.accept("{\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0," + + "\"id\":\"call_1\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\"}}]}," + + "\"finish_reason\":null}]}")); + assembler.accept("{\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0," + + "\"function\":{\"arguments\":\"{}\"}}]},\"finish_reason\":\"tool_calls\"}]}"); + ChatResponse response = assembler.complete(); + + assertThat(response.aiMessage().toolExecutionRequests().get(0).name(), is("get_weather")); + assertThat(response.aiMessage().toolExecutionRequests().get(0).arguments(), is("{}")); + // The header delta is withheld; the id and name it carried go with the first argument fragment. + assertThat(handler.partialToolCalls.size(), is(1)); + assertThat(handler.partialToolCalls.get(0).index(), is(0)); + assertThat(handler.partialToolCalls.get(0).id(), is("call_1")); + assertThat(handler.partialToolCalls.get(0).name(), is("get_weather")); + assertThat(handler.partialToolCalls.get(0).partialArguments(), is("{}")); + assertThat(handler.completeToolCalls.size(), is(1)); + assertPartialsMakeUpTheCompleteCall(handler); + } + + /** + * llama.cpp's incremental parser may discover a tool call before its name ({@code compute_diffs} + * allows the name to go from empty to non-empty); {@link PartialToolCall} refuses a blank name. + */ + @Test + void toolCallDeltaBeforeItsName_doesNotFailTheStream() { + RecordingHandler handler = new RecordingHandler(); + StreamingChunkAssembler assembler = new StreamingChunkAssembler(handler); + + assertDoesNotThrow(() -> assembler.accept("{\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0," + + "\"function\":{\"arguments\":\"{\\\"ci\"}}]},\"finish_reason\":null}]}")); + assembler.accept("{\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0," + + "\"id\":\"call_1\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"ty\\\":1}\"}}]}," + + "\"finish_reason\":\"tool_calls\"}]}"); + ChatResponse response = assembler.complete(); + + assertThat(response.aiMessage().toolExecutionRequests().get(0).name(), is("get_weather")); + assertThat(response.aiMessage().toolExecutionRequests().get(0).arguments(), is("{\"city\":1}")); + // The prefix that arrived before the name is reported as soon as the name is known, so the + // partials make up the arguments, each with the final id and name. + assertThat(handler.completeToolCalls.size(), is(1)); + assertThat(handler.partialToolCalls, is(not(empty()))); + assertPartialsMakeUpTheCompleteCall(handler); + } + @Test void assemblesParallelToolCallsByIndex() { RecordingHandler handler = new RecordingHandler(); @@ -130,10 +209,90 @@ void assemblesParallelToolCallsByIndex() { ChatResponse response = assembler.complete(); assertThat(handler.completeToolCalls.size(), is(2)); + // Call 0 is complete once the fragment of call 1 arrives, even inside one chunk. + assertThat(handler.toolCallEvents, contains("partial:0", "complete:0", "partial:1", "complete:1")); + assertThat(response.aiMessage().toolExecutionRequests().get(0).name(), is("first")); + assertThat(response.aiMessage().toolExecutionRequests().get(1).name(), is("second")); + } + + /** + * llama.cpp only ever extends the last tool call of a message ({@code compute_diffs} in + * {@code common/chat.cpp}), so call 0 is final once call 1 starts. langchain4j's handler contract + * and its own OpenAI binding complete a call at that point, before the next call's first partial, + * so that a handler can run the tool at once. + */ + @Test + void completesEachToolCallAsSoonAsTheNextOneStarts() { + RecordingHandler handler = new RecordingHandler(); + StreamingChunkAssembler assembler = new StreamingChunkAssembler(handler); + + assembler.accept("{\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0," + + "\"id\":\"a\",\"function\":{\"name\":\"first\",\"arguments\":\"{\\\"x\\\":1}\"}}]}," + + "\"finish_reason\":null}]}"); + assembler.accept("{\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1," + + "\"id\":\"b\",\"function\":{\"name\":\"second\",\"arguments\":\"{\\\"y\\\":2}\"}}]}," + + "\"finish_reason\":null}]}"); + assembler.accept("{\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"tool_calls\"}]}"); + ChatResponse response = assembler.complete(); + + assertThat(handler.toolCallEvents, contains("partial:0", "complete:0", "partial:1", "complete:1")); + assertThat(handler.completeToolCalls.get(0).toolExecutionRequest(), + is(response.aiMessage().toolExecutionRequests().get(0))); + assertThat(handler.completeToolCalls.get(1).toolExecutionRequest(), + is(response.aiMessage().toolExecutionRequests().get(1))); + assertThat(response.aiMessage().toolExecutionRequests().get(0).arguments(), is("{\"x\":1}")); + assertThat(response.aiMessage().toolExecutionRequests().get(1).arguments(), is("{\"y\":2}")); + } + + /** + * A call that has no name yet when the next one starts is not completed early: a + * {@link CompleteToolCall} with a blank name would reach the handler. It is completed by + * {@code complete()}, with the name that arrived in the meantime; the arguments it held back are + * reported as its partial when the name arrives, before its completion. + */ + @Test + void defersCompletionOfAToolCallThatHasNoNameYetWhenTheNextOneStarts() { + RecordingHandler handler = new RecordingHandler(); + StreamingChunkAssembler assembler = new StreamingChunkAssembler(handler); + + assembler.accept("{\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0," + + "\"function\":{\"arguments\":\"{}\"}}]},\"finish_reason\":null}]}"); + assembler.accept("{\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":1," + + "\"id\":\"b\",\"function\":{\"name\":\"second\",\"arguments\":\"{}\"}}]}," + + "\"finish_reason\":null}]}"); + assembler.accept("{\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0," + + "\"id\":\"a\",\"function\":{\"name\":\"first\"}}]},\"finish_reason\":\"tool_calls\"}]}"); + ChatResponse response = assembler.complete(); + + assertThat(handler.toolCallEvents, contains("partial:1", "partial:0", "complete:0", "complete:1")); + assertThat(handler.partialToolCalls.get(1).partialArguments(), is("{}")); + assertThat(handler.partialToolCalls.get(1).name(), is("first")); + assertThat(handler.completeToolCalls.get(0).toolExecutionRequest().name(), is("first")); assertThat(response.aiMessage().toolExecutionRequests().get(0).name(), is("first")); assertThat(response.aiMessage().toolExecutionRequests().get(1).name(), is("second")); } + /** + * Arguments for a call that was already reported complete would make the final request differ + * from the {@link CompleteToolCall} the handler got; that is a native contract violation and + * fails the stream instead of being merged silently. + */ + @Test + void argumentsForAToolCallThatIsAlreadyCompleteFailTheStream() { + RecordingHandler handler = new RecordingHandler(); + StreamingChunkAssembler assembler = new StreamingChunkAssembler(handler); + assembler.accept("{\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[" + + "{\"index\":0,\"id\":\"a\",\"function\":{\"name\":\"first\",\"arguments\":\"{\"}}," + + "{\"index\":1,\"id\":\"b\",\"function\":{\"name\":\"second\",\"arguments\":\"{}\"}}" + + "]},\"finish_reason\":null}]}"); + + IllegalStateException e = assertThrows(IllegalStateException.class, () -> assembler.accept( + "{\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0," + + "\"function\":{\"arguments\":\"}\"}}]},\"finish_reason\":null}]}")); + + assertThat(e.getMessage(), containsString("tool call 0")); + } + @Test void forwardsThinkingDeltasAndKeepsThinkingOnFinalMessage() { RecordingHandler handler = new RecordingHandler(); @@ -187,6 +346,38 @@ void noUsageChunkMeansNoTokenUsage() { assertThat(response.tokenUsage(), is(nullValue())); } + /** + * With {@code include_usage} (which the adapter requests) upstream sends the usage in a chunk of + * its own after the finish chunk, with an empty {@code choices} array as the OpenAI spec asks, + * carrying {@code timings} as well ({@code to_json_oaicompat_chat_stream} in + * {@code tools/server/server-task.cpp}). The usage must be read from it, and the finish reason + * of the chunk before must survive a chunk that has no choice. + */ + @Test + void usageChunkWithEmptyChoicesCarriesTokenUsage() { + RecordingHandler handler = new RecordingHandler(); + StreamingChunkAssembler assembler = new StreamingChunkAssembler(handler); + + assembler.accept(contentChunk("ok")); + assembler.accept("{\"choices\":[{\"finish_reason\":\"length\",\"index\":0,\"delta\":{}}]," + + "\"created\":1760000000,\"id\":\"chatcmpl-abc\",\"model\":\"m\"," + + "\"system_fingerprint\":\"b11639-a0e6dc738\",\"object\":\"chat.completion.chunk\"}"); + assembler.accept("{\"choices\":[],\"created\":1760000000,\"id\":\"chatcmpl-abc\",\"model\":\"m\"," + + "\"system_fingerprint\":\"b11639-a0e6dc738\",\"object\":\"chat.completion.chunk\"," + + "\"usage\":{\"completion_tokens\":3,\"prompt_tokens\":7,\"total_tokens\":10," + + "\"prompt_tokens_details\":{\"cached_tokens\":0}}," + + "\"timings\":{\"cache_n\":0,\"prompt_n\":7,\"predicted_n\":3}}"); + ChatResponse response = assembler.complete(); + + assertThat(response.tokenUsage(), is(notNullValue())); + assertThat(response.tokenUsage().inputTokenCount(), is(7)); + assertThat(response.tokenUsage().outputTokenCount(), is(3)); + // "length", not "stop": no finish reason maps to STOP, so a reset would hide behind "stop". + assertThat(response.finishReason(), is(FinishReason.LENGTH)); + assertThat(handler.partials, contains("ok")); + assertThat(response.aiMessage().text(), is("ok")); + } + @Test void unparseableChunkFailsLoud() { StreamingChunkAssembler assembler = new StreamingChunkAssembler(new RecordingHandler()); diff --git a/llama/CMakeLists.txt b/llama/CMakeLists.txt index d51118bd4..fd17d9568 100644 --- a/llama/CMakeLists.txt +++ b/llama/CMakeLists.txt @@ -213,8 +213,11 @@ set(LLAMA_BUILD_COMMON ON) # cpp-httplib built with ONE setting. Off where BoringSSL is not built here: Android (the dockcross # cross-clang, and the AAR has no download use case), s390x (big-endian is outside BoringSSL's # supported set) and the Linux SYCL builds (icx/icpx, compiler id IntelLLVM, which BoringSSL's CMake -# treats as neither GCC nor Clang; upstream's ubuntu-24-sycl release job passes LLAMA_OPENSSL=OFF too, -# while its windows-sycl job builds BoringSSL -- as this file did before). LLAMA_OPENSSL=OFF there +# treats as neither GCC nor Clang; upstream's ubuntu-24-sycl release job builds against the system +# OpenSSL instead since b11676/#30456 -- libssl-dev, a dynamic libssl, which verify-native-deps.py +# would reject in a shipped library -- while its windows-sycl job builds BoringSSL, as this file does. +# The SYCL jars themselves are module-only builds, ggml alone and no cpp-httplib, so no SSL code +# reaches them either way; the setting matters only for a full Linux SYCL build). LLAMA_OPENSSL=OFF there # Linux: no libgomp dependency, because the jar has to load in the images Java is actually deployed # in. `libggml-base.so` -- the first file LlamaLoader loads -- declares `libgomp.so.1` whenever ggml # builds with OpenMP, and GCC's -fopenmp links it DYNAMICALLY, so it is a runtime requirement of the @@ -344,8 +347,8 @@ endif() # hash, every other consumer keeps the number, and the pin check after FetchContent_MakeAvailable # asserts that the fetched commit's count IS the number. Clear it once the tag exists; # `llama-next-version.sh` says when, and `llama-next-version.sh --resolve b` prints the hash. -set(JLLAMA_GIT_TAG b11639) -set(JLLAMA_GIT_COMMIT "a0e6dc7384edb8f5d927155e1d26c10b4a310e6d") # the commit b11639 names; kept after the tag exists +set(JLLAMA_GIT_TAG b11815) +set(JLLAMA_GIT_COMMIT "7170cad4d02f7907517613931840cbb2367399a3") # the commit b11815 names; kept after the tag exists if(NOT JLLAMA_GIT_TAG MATCHES "^b[0-9]+$") message(FATAL_ERROR "JLLAMA_GIT_TAG must be a llama.cpp release number b, got '${JLLAMA_GIT_TAG}'") endif() @@ -948,6 +951,12 @@ if(NOT ANDROID_ABI AND NOT OS_NAME MATCHES "Android") ${llama.cpp_SOURCE_DIR}/tools/server/server-mcp.cpp ${llama.cpp_SOURCE_DIR}/tools/server/server-tools.cpp ${llama.cpp_SOURCE_DIR}/tools/server/server.cpp + # server-connect.cpp (b11744, #28277): server.cpp spawns the llama-connect helper binary through + # it when --connect is given and references the class unconditionally, so the unit is a link + # requirement of server.cpp, never of the JNI routes. Without the binary --connect fails with + # a message; a build without subprocess support (Android, where server.cpp is not compiled + # anyway) answers "subprocess support disabled". + ${llama.cpp_SOURCE_DIR}/tools/server/server-connect.cpp ${CMAKE_SOURCE_DIR}/src/main/cpp/native_server_engine.cpp ${CMAKE_SOURCE_DIR}/src/main/cpp/native_server.cpp ) @@ -1350,6 +1359,7 @@ if(BUILD_TESTING) src/test/cpp/test_wire_contracts.cpp src/test/cpp/test_rpc.cpp src/test/cpp/test_gallocr_leafs.cpp + src/test/cpp/test_lora_cache.cpp ${llama.cpp_SOURCE_DIR}/tools/server/server-common.cpp ${llama.cpp_SOURCE_DIR}/tools/server/server-chat.cpp ${llama.cpp_SOURCE_DIR}/tools/server/server-context.cpp @@ -1391,6 +1401,7 @@ if(BUILD_TESTING) src/test/cpp/test_native_server_shutdown.cpp src/main/cpp/native_server_engine.cpp ${llama.cpp_SOURCE_DIR}/tools/server/server.cpp + ${llama.cpp_SOURCE_DIR}/tools/server/server-connect.cpp ${llama.cpp_SOURCE_DIR}/tools/server/server-tools.cpp ${llama.cpp_SOURCE_DIR}/tools/server/server-mcp.cpp ${llama.cpp_SOURCE_DIR}/tools/server/server-http.cpp diff --git a/llama/patches/0007-server-attach-http-frontend.patch b/llama/patches/0007-server-attach-http-frontend.patch index 91d97f66b..d80f18e66 100644 --- a/llama/patches/0007-server-attach-http-frontend.patch +++ b/llama/patches/0007-server-attach-http-frontend.patch @@ -12,11 +12,15 @@ llama_server_request_shutdown(). It serves the caller's server_routes rather tha server_routes' constructor registers a sleeping-state callback on the model's task queue that outlives the attach call. Applies after 0001 and 0006. +Refreshed at b11799: POST /tts (b11781, #26603) and the WebSocket /v1/realtime (b11780, #30636) joined +the route table between the transcription and the Anthropic routes; both are in the removed block and +in the helper, so attach mode serves them too. + diff --git a/tools/server/server.cpp b/tools/server/server.cpp -index 4c6052d78..cb03f239a 100644 +index d3131f206..287d6708e 100644 --- a/tools/server/server.cpp +++ b/tools/server/server.cpp -@@ -90,6 +90,7 @@ int llama_server(int argc, char ** argv); +@@ -91,6 +91,7 @@ int llama_server(int argc, char ** argv); // to be used via CLI (argc / argv are used by router mode only) int llama_server(common_params & params, int argc, char ** argv); int llama_server(common_params & params, int argc, char ** argv, server_child & child); @@ -24,7 +28,7 @@ index 4c6052d78..cb03f239a 100644 void llama_server_terminate(); void llama_server_terminate() { if (shutdown_handler) { -@@ -138,6 +139,58 @@ static server_http_context::handler_t ex_wrapper(server_http_context::handler_t +@@ -139,6 +140,60 @@ static server_http_context::handler_t ex_wrapper(server_http_context::handler_t }; } @@ -53,6 +57,8 @@ index 4c6052d78..cb03f239a 100644 + ctx_http.post("/responses", ex_wrapper(routes.post_responses_oai)); + ctx_http.post("/v1/audio/transcriptions", ex_wrapper(routes.post_transcriptions_oai)); + ctx_http.post("/audio/transcriptions", ex_wrapper(routes.post_transcriptions_oai)); ++ ctx_http.post("/tts", ex_wrapper(routes.post_tts)); ++ ctx_http.ws ("/v1/realtime", routes.ws_realtime); + ctx_http.post("/v1/messages", ex_wrapper(routes.post_anthropic_messages)); // anthropic messages API + ctx_http.post("/infill", ex_wrapper(routes.post_infill)); + ctx_http.post("/embedding", ex_wrapper(routes.post_embeddings)); // legacy @@ -83,7 +89,7 @@ index 4c6052d78..cb03f239a 100644 int llama_server(int argc, char ** argv) { server_child child; -@@ -317,48 +370,7 @@ int llama_server(common_params & params, int argc, char ** argv, server_child & +@@ -334,50 +389,7 @@ int llama_server(common_params & params, int argc, char ** argv, server_child & ctx_http.del ("/models", ex_wrapper(models_routes->del_router_models)); } @@ -104,6 +110,8 @@ index 4c6052d78..cb03f239a 100644 - ctx_http.post("/responses", ex_wrapper(routes.post_responses_oai)); - ctx_http.post("/v1/audio/transcriptions", ex_wrapper(routes.post_transcriptions_oai)); - ctx_http.post("/audio/transcriptions", ex_wrapper(routes.post_transcriptions_oai)); +- ctx_http.post("/tts", ex_wrapper(routes.post_tts)); +- ctx_http.ws ("/v1/realtime", routes.ws_realtime); - ctx_http.post("/v1/messages", ex_wrapper(routes.post_anthropic_messages)); // anthropic messages API - ctx_http.post("/infill", ex_wrapper(routes.post_infill)); - ctx_http.post("/embedding", ex_wrapper(routes.post_embeddings)); // legacy @@ -133,7 +141,7 @@ index 4c6052d78..cb03f239a 100644 // resumable streaming: a child binds the local session factories, the router binds // proxies that resolve the owning child, see server-stream.h -@@ -628,3 +640,98 @@ int llama_server(common_params & params, int argc, char ** argv, server_child & +@@ -653,3 +665,98 @@ int llama_server(common_params & params, int argc, char ** argv, server_child & return 0; } diff --git a/llama/patches/0016-model-kolibri1.patch b/llama/patches/0016-model-kolibri1.patch deleted file mode 100644 index ab501edf4..000000000 --- a/llama/patches/0016-model-kolibri1.patch +++ /dev/null @@ -1,389 +0,0 @@ -model: add Kolibri-1 (kolibri1) -- carried until upstream supports it - -Aleph Alpha's Kolibri-1 is a 78B-parameter German/English reasoning MoE (3.46B active per -token, Apache-2.0). Upstream llama.cpp has no support yet; the request is -https://github.com/ggml-org/llama.cpp/issues/29922 (open, no PR at the time of writing). -DROP THIS PATCH as soon as upstream registers the architecture -- check on every bump with -`git grep -n kolibri src/llama-arch.cpp` in the new llama.cpp tag. This one is a clean true/false, -unlike the drop-checks of 0017, 0018 and 0019: the name reaches that file only through this patch, -so NO hits means the patch is still needed and any hit means upstream has it. Run it against the -pristine tag, never against a patched source tree, which would show this patch's own lines. -Measured at b11639 (a0e6dc738): 0 hits. - -Architecture (from Aleph Alpha's own vLLM implementation, the reference used here): - - GQA attention with per-head q/k RMSNorm. config.layer_types interleaves sliding-window - layers, which use (NEOX) RoPE, with full-attention layers that have no positional encoding. - - Sandwich norms: input norm -> attention -> post-attention norm -> residual, then - pre-FFN norm -> routed MoE + one ungated shared expert -> post-FFN norm -> residual. - - Router: top-k selected on (logits + expert_bias), experts weighted by the unbiased - sigmoid(logits), renormalized only if norm_topk_prob (off in Kolibri-1). This is NOT - DeepSeek-V3's sigmoid router (selection on sigmoid(logits) + bias), which picks other - experts as soon as the bias is non-zero -- so the graph builds the selection itself and - hands it to build_moe_ffn (selected_experts_in); build_moe_ffn itself is unchanged. - -What the patch touches: the arch name "kolibri1" (llama-arch.{h,cpp}), the model mapping and -NEOX rope type (llama-model.cpp), the model struct (models/models.h), the new -src/models/kolibri1.cpp, and the "kolibri1" pre-tokenizer name mapped to Qwen2's -(llama-vocab.cpp). No converter or gguf-py change: this project only loads GGUFs. - -Both published GGUF dialects load. The two community converters disagree on metadata, and -each community runtime rejects the other's files: - - expert_gating_func: 2 (sigmoid) from the AFMoE-based converter, 5 ("sigmoid_logit_add", - a value only the Qwen3-MoE-based converter's patch defines) from the other one, or - absent. All three mean the same router, which is a property of the architecture. - - tokenizer.ggml.pre: "qwen2" or "kolibri1"; the Qwen3-MoE-based runtime registers the - latter, the AFMoE-based one does not (llama.cpp rejects an unknown pre-tokenizer). - - expert_weights_norm, expert_shared_count and output.weight are optional here. - -Known places (all read while writing this patch, 2026-10-06): - - Request: https://github.com/ggml-org/llama.cpp/issues/29922 - - Model: https://huggingface.co/Aleph-Alpha/Kolibri-1 (and Kolibri-1-BF16) - - Reference: https://github.com/Aleph-Alpha/aleph-alpha-inference, aleph_alpha_inference/ - kolibri1.py at 049a6a7 (Apache-2.0, Copyright 2026 Aleph Alpha GmbH) -- the - specification this patch follows; its tests/test_kolibri1.py pins the router. - - AFMoE-based port by Eliasfpv28, base llama.cpp edd6e2bbd (b11378): - https://huggingface.co/Eliasfpv28/Kolibri-1-Q3_K_S-GGUF, runtime-source/ - kolibri1-runtime.patch (new additions Apache-2.0, llama.cpp-derived parts MIT). - Source of the graph-side router (selection built outside build_moe_ffn). - Its runtime is also what https://huggingface.co/InsidiousFiddler/Kolibri-1-GGUF - points to. - - Qwen3-MoE-based port, base llama.cpp 836d571 (b11381), as carried in - https://github.com/mjwsolo/localcode/pull/101 (patches/0006-kolibri1.patch, - "community patch"). https://huggingface.co/Hob-forge/Kolibri-1-GGUF publishes - GGUFs and a kolibri1-llama.cpp.patch (MIT) for the same base commit whose model - card describes the same design (a new gating mode); that file itself could not be - fetched from here, so it is not verified to be byte-identical. Source of the - loader (optional keys, tied-output fallback, swa rope base) and of the graph layout. - - Independent effort under the name "kolibri" (not "kolibri1"), whose GGUFs this patch - does not load: https://github.com/CWBudde/kolibri-llama-cpp, - https://github.com/CWBudde/llama.cpp/pull/7 - -Verification in java-llama.cpp: src/test/cpp/test_kolibri1.cpp builds tiny random Kolibri-1 -GGUFs in both dialects, runs them through the real library and compares every logit with an -independent float reference written from the vLLM implementation above. - -diff --git a/src/llama-arch.cpp b/src/llama-arch.cpp -index eea2ef589..638d73c4b 100644 ---- a/src/llama-arch.cpp -+++ b/src/llama-arch.cpp -@@ -117,6 +117,7 @@ static const std::map LLM_ARCH_NAMES = { - { LLM_ARCH_DOTS3NOTE, "dots3note" }, - { LLM_ARCH_ARCEE, "arcee" }, - { LLM_ARCH_AFMOE, "afmoe" }, -+ { LLM_ARCH_KOLIBRI1, "kolibri1" }, - { LLM_ARCH_LAGUNA, "laguna" }, - { LLM_ARCH_ERNIE4_5, "ernie4_5" }, - { LLM_ARCH_ERNIE4_5_MOE, "ernie4_5-moe" }, -diff --git a/src/llama-arch.h b/src/llama-arch.h -index c8abee234..6a235d2b1 100644 ---- a/src/llama-arch.h -+++ b/src/llama-arch.h -@@ -122,6 +122,7 @@ enum llm_arch { - LLM_ARCH_DOTS3NOTE, - LLM_ARCH_ARCEE, - LLM_ARCH_AFMOE, -+ LLM_ARCH_KOLIBRI1, - LLM_ARCH_LAGUNA, - LLM_ARCH_ERNIE4_5, - LLM_ARCH_ERNIE4_5_MOE, -diff --git a/src/llama-model.cpp b/src/llama-model.cpp -index 608285d5c..03150375d 100644 ---- a/src/llama-model.cpp -+++ b/src/llama-model.cpp -@@ -276,6 +276,8 @@ static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params - return new llama_model_arcee(params); - case LLM_ARCH_AFMOE: - return new llama_model_afmoe(params); -+ case LLM_ARCH_KOLIBRI1: -+ return new llama_model_kolibri1(params); - case LLM_ARCH_LAGUNA: - return new llama_model_laguna(params); - case LLM_ARCH_ERNIE4_5: -@@ -3254,6 +3256,7 @@ llama_rope_type llama_model_rope_type(const llama_model * model) { - case LLM_ARCH_COGVLM: - case LLM_ARCH_PANGU_EMBED: - case LLM_ARCH_AFMOE: -+ case LLM_ARCH_KOLIBRI1: - case LLM_ARCH_LAGUNA: - case LLM_ARCH_QWEN3NEXT: - case LLM_ARCH_MIMO2: -diff --git a/src/llama-vocab.cpp b/src/llama-vocab.cpp -index de20c757f..5b54a31bf 100644 ---- a/src/llama-vocab.cpp -+++ b/src/llama-vocab.cpp -@@ -2376,7 +2376,8 @@ void llama_vocab::impl::load(llama_model_loader & ml, const LLM_KV & kv) { - tokenizer_pre == "qwen2" || - tokenizer_pre == "deepseek-r1-qwen" || - tokenizer_pre == "kormo" || -- tokenizer_pre == "f2llmv2") { -+ tokenizer_pre == "f2llmv2" || -+ tokenizer_pre == "kolibri1") { - pre_type = LLAMA_VOCAB_PRE_TYPE_QWEN2; - clean_spaces = false; - } else if ( -diff --git a/src/models/kolibri1.cpp b/src/models/kolibri1.cpp -new file mode 100644 -index 000000000..fb52a7a4b ---- /dev/null -+++ b/src/models/kolibri1.cpp -@@ -0,0 +1,237 @@ -+#include "models.h" -+ -+// Aleph Alpha Kolibri 1 (https://huggingface.co/Aleph-Alpha/Kolibri-1), a German/English reasoning MoE. -+// Reference: aleph_alpha_inference/kolibri1.py (https://github.com/Aleph-Alpha/aleph-alpha-inference, 049a6a7). -+// -+// - attention: GQA with per-head q/k RMSNorm; layer_types interleave sliding-window layers, which use -+// (NEOX) RoPE, with full-attention layers, which use no positional encoding at all (RNoPE) -+// - sandwich norms: input norm -> attention -> post-attention norm -> residual, -+// pre-FFN norm -> MoE + shared expert -> post-FFN norm -> residual -+// - every layer is MoE plus one ungated shared expert, added to the routed output -+// - router: top-k is selected on (logits + expert_bias) and the experts are weighted by the *unbiased* -+// sigmoid(logits), renormalized only if norm_topk_prob is set (Kolibri 1 ships it off). This is not -+// DeepSeek-V3's sigmoid router, which selects on sigmoid(logits) + bias -- that one picks different -+// experts as soon as the bias is non-zero, so the selection is built here and handed to build_moe_ffn. -+// -+// Two community converters produced the published GGUFs, and they disagree on metadata; both load: -+// - expert_gating_func: 2 (sigmoid) from the AFMoE-based converter, 5 ("sigmoid_logit_add", a value that -+// exists only in the other converter's patch) from the Qwen3-MoE-based one, or absent. The routing is a -+// property of the architecture, so the value is only checked, never used to pick another router. -+// - tokenizer.ggml.pre: "qwen2" or "kolibri1" (llama-vocab.cpp maps both to the Qwen2 pre-tokenizer). -+// - expert_weights_norm and output.weight are optional (defaults: no renormalization, tied embeddings). -+ -+void llama_model_kolibri1::load_arch_hparams(llama_model_loader & ml) { -+ ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps); -+ ml.get_key_or_arr(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp_arr, hparams.n_layer_all); -+ ml.get_key(LLM_KV_EXPERT_SHARED_FEED_FORWARD_LENGTH, hparams.n_ff_shexp); -+ ml.get_key(LLM_KV_ATTENTION_SLIDING_WINDOW, hparams.n_swa); -+ -+ hparams.n_expert_shared = 1; -+ ml.get_key(LLM_KV_EXPERT_SHARED_COUNT, hparams.n_expert_shared, false); -+ ml.get_key(LLM_KV_EXPERT_WEIGHTS_NORM, hparams.expert_weights_norm, false); -+ -+ uint32_t gating_func = LLAMA_EXPERT_GATING_FUNC_TYPE_SIGMOID; -+ ml.get_key(LLM_KV_EXPERT_GATING_FUNC, gating_func, false); -+ if (gating_func != LLAMA_EXPERT_GATING_FUNC_TYPE_SIGMOID && gating_func != 5) { -+ throw std::runtime_error(format("kolibri1: unexpected expert_gating_func %u (expected 2 or 5)", gating_func)); -+ } -+ // what print_info reports; the selection on logits + bias is built in the graph -+ hparams.expert_gating_func = LLAMA_EXPERT_GATING_FUNC_TYPE_SIGMOID; -+ -+ if (hparams.n_swa == 0) { -+ throw std::runtime_error("kolibri1: sliding_window must be > 0"); -+ } -+ if (hparams.n_expert_shared != 1 || hparams.n_ff_shexp == 0) { -+ throw std::runtime_error(format("kolibri1: expected one shared expert, got %u of length %u", -+ hparams.n_expert_shared, hparams.n_ff_shexp)); -+ } -+ -+ // the converters write the exact per-layer pattern from config.layer_types; 4 sliding + 1 full otherwise -+ hparams.swa_type = LLAMA_SWA_TYPE_STANDARD; -+ load_swa_pattern(ml, 5); -+ -+ // only the sliding-window layers use RoPE, with the model's single rope_theta -+ hparams.rope_freq_base_train_swa = hparams.rope_freq_base_train; -+ hparams.rope_freq_scale_train_swa = hparams.rope_freq_scale_train; -+ ml.get_key(LLM_KV_ROPE_FREQ_BASE_SWA, hparams.rope_freq_base_train_swa, false); -+ -+ type = LLM_TYPE_UNKNOWN; -+} -+ -+void llama_model_kolibri1::load_arch_tensors(llama_model_loader &) { -+ LLAMA_LOAD_LOCALS; -+ -+ if (n_expert == 0 || n_expert_used == 0) { -+ throw std::runtime_error("kolibri1: n_expert and n_expert_used must be > 0"); -+ } -+ -+ tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, 0); -+ -+ output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"), {n_embd}, 0); -+ output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"), {n_embd, n_vocab}, TENSOR_NOT_REQUIRED); -+ if (output == NULL) { -+ output = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"), {n_embd, n_vocab}, TENSOR_DUPLICATED); -+ } -+ -+ const int64_t n_ff_exp = hparams.n_ff_exp(); -+ const int64_t n_ff_shexp = hparams.n_ff_shexp; -+ -+ for (int i = 0; i < n_layer; ++i) { -+ auto & layer = layers[i]; -+ -+ layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i), {n_embd}, 0); -+ layer.attn_post_norm = create_tensor(tn(LLM_TENSOR_ATTN_POST_NORM, "weight", i), {n_embd}, 0); -+ -+ create_tensor_qkv(layer, i, n_embd, n_embd_head_k * n_head, n_embd_k_gqa, n_embd_v_gqa, 0); -+ layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", i), {n_embd_head_k * n_head, n_embd}, 0); -+ -+ layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", i), {n_embd_head_k}, 0); -+ layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", i), {n_embd_head_k}, 0); -+ -+ layer.ffn_norm = create_tensor(tn(LLM_TENSOR_FFN_NORM, "weight", i), {n_embd}, 0); -+ layer.ffn_post_norm = create_tensor(tn(LLM_TENSOR_FFN_POST_NORM, "weight", i), {n_embd}, 0); -+ -+ layer.ffn_gate_inp = create_tensor(tn(LLM_TENSOR_FFN_GATE_INP, "weight", i), {n_embd, n_expert}, 0); -+ layer.ffn_exp_probs_b = create_tensor(tn(LLM_TENSOR_FFN_EXP_PROBS_B, "bias", i), {n_expert}, 0); -+ -+ layer.ffn_gate_exps = create_tensor(tn(LLM_TENSOR_FFN_GATE_EXPS, "weight", i), { n_embd, n_ff_exp, n_expert}, 0); -+ layer.ffn_down_exps = create_tensor(tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", i), {n_ff_exp, n_embd, n_expert}, 0); -+ layer.ffn_up_exps = create_tensor(tn(LLM_TENSOR_FFN_UP_EXPS, "weight", i), { n_embd, n_ff_exp, n_expert}, 0); -+ -+ layer.ffn_gate_shexp = create_tensor(tn(LLM_TENSOR_FFN_GATE_SHEXP, "weight", i), { n_embd, n_ff_shexp}, 0); -+ layer.ffn_down_shexp = create_tensor(tn(LLM_TENSOR_FFN_DOWN_SHEXP, "weight", i), {n_ff_shexp, n_embd}, 0); -+ layer.ffn_up_shexp = create_tensor(tn(LLM_TENSOR_FFN_UP_SHEXP, "weight", i), { n_embd, n_ff_shexp}, 0); -+ } -+} -+ -+std::unique_ptr llama_model_kolibri1::build_arch_graph(const llm_graph_params & params) const { -+ return std::make_unique(*this, params); -+} -+ -+llama_model_kolibri1::graph::graph(const llama_model & model, const llm_graph_params & params) : llm_graph_context(params) { -+ const int64_t n_embd_head = hparams.n_embd_head_v(); -+ GGML_ASSERT(n_embd_head == hparams.n_embd_head_k()); -+ -+ ggml_tensor * cur; -+ ggml_tensor * inpL = build_inp_embd(model.tok_embd); -+ -+ ggml_tensor * inp_pos = build_inp_pos(); -+ auto * inp_attn = build_attn_inp_kv_iswa(); -+ ggml_tensor * inp_out_ids = build_inp_out_ids(); -+ -+ const float kq_scale = 1.0f/sqrtf(float(n_embd_head)); -+ -+ for (int il = 0; il < n_layer; ++il) { -+ const auto & layer = model.layers[il]; -+ -+ ggml_tensor * inpSA = inpL; -+ -+ cur = build_norm(inpL, layer.attn_norm, NULL, LLM_NORM_RMS, il); -+ cb(cur, "attn_norm", il); -+ -+ // self-attention -+ { -+ auto [Qcur, Kcur, Vcur] = build_qkv(layer, cur, n_embd_head, n_head, n_head_kv, il); -+ -+ Qcur = build_norm(Qcur, layer.attn_q_norm, NULL, LLM_NORM_RMS, il); -+ Kcur = build_norm(Kcur, layer.attn_k_norm, NULL, LLM_NORM_RMS, il); -+ cb(Qcur, "Qcur_normed", il); -+ cb(Kcur, "Kcur_normed", il); -+ -+ // sliding-window layers: RoPE; full-attention layers: no positional encoding -+ if (hparams.is_swa(il)) { -+ const float freq_base_l = model.get_rope_freq_base (cparams, il); -+ const float freq_scale_l = model.get_rope_freq_scale(cparams, il); -+ -+ Qcur = ggml_rope_ext(ctx0, Qcur, inp_pos, nullptr, -+ n_rot, rope_type, n_ctx_orig, freq_base_l, freq_scale_l, -+ ext_factor, attn_factor, beta_fast, beta_slow); -+ Kcur = ggml_rope_ext(ctx0, Kcur, inp_pos, nullptr, -+ n_rot, rope_type, n_ctx_orig, freq_base_l, freq_scale_l, -+ ext_factor, attn_factor, beta_fast, beta_slow); -+ cb(Qcur, "Qcur_rope", il); -+ cb(Kcur, "Kcur_rope", il); -+ } -+ -+ cur = build_attn(inp_attn, -+ layer.wo, NULL, layer.wo_s, -+ Qcur, Kcur, Vcur, nullptr, nullptr, nullptr, kq_scale, il); -+ cb(cur, "attn_out", il); -+ } -+ -+ cur = build_norm(cur, layer.attn_post_norm, NULL, LLM_NORM_RMS, il); -+ cb(cur, "attn_post_norm", il); -+ -+ if (il == n_layer - 1 && inp_out_ids) { -+ cur = ggml_get_rows(ctx0, cur, inp_out_ids); -+ inpSA = ggml_get_rows(ctx0, inpSA, inp_out_ids); -+ } -+ -+ ggml_tensor * ffn_inp = ggml_add(ctx0, cur, inpSA); -+ cb(ffn_inp, "ffn_inp", il); -+ -+ // HF "post_attention_layernorm" is the pre-FFN norm here -+ cur = build_norm(ffn_inp, layer.ffn_norm, NULL, LLM_NORM_RMS, il); -+ cb(cur, "ffn_norm", il); -+ -+ // router: select on logits + expert_bias, weight by the unbiased sigmoid(logits). The reference -+ // computes the logits in fp32 (GateLinear out_dtype=float32), and the selection is sensitive to it: -+ // the biases reach ~20, so keep both the accumulation and the activations at F32 on every backend. -+ ggml_tensor * logits = build_lora_mm(layer.ffn_gate_inp, cur); // [n_expert, n_tokens] -+ ggml_prec_set_acc(logits, GGML_PREC_F32); -+ ggml_prec_set_src(logits, GGML_PREC_F32, 1); -+ cb(logits, "ffn_moe_logits", il); -+ -+ ggml_tensor * selection = ggml_add(ctx0, logits, layer.ffn_exp_probs_b); -+ cb(selection, "ffn_moe_logits_biased", il); -+ -+ ggml_tensor * selected = ggml_argsort_top_k(ctx0, selection, n_expert_used); // [n_expert_used, n_tokens] -+ -+ ggml_tensor * moe_out = build_moe_ffn(cur, -+ layer.ffn_gate_inp, -+ layer.ffn_up_exps, -+ layer.ffn_gate_exps, -+ layer.ffn_down_exps, -+ nullptr, // the bias already went into the selection above -+ n_expert, n_expert_used, -+ LLM_FFN_SILU, -+ hparams.expert_weights_norm, -+ 0.0f, // no routed scaling factor -+ LLAMA_EXPERT_GATING_FUNC_TYPE_SIGMOID, -+ il, -+ logits, -+ nullptr, nullptr, nullptr, nullptr, -+ selected); -+ cb(moe_out, "ffn_moe_out", il); -+ -+ ggml_tensor * ffn_shexp = build_ffn(cur, -+ layer.ffn_up_shexp, NULL, NULL, -+ layer.ffn_gate_shexp, NULL, NULL, -+ layer.ffn_down_shexp, NULL, NULL, -+ NULL, -+ LLM_FFN_SILU, LLM_FFN_PAR, il); -+ cb(ffn_shexp, "ffn_shexp", il); -+ -+ cur = ggml_add(ctx0, moe_out, ffn_shexp); -+ cb(cur, "ffn_out", il); -+ -+ cur = build_norm(cur, layer.ffn_post_norm, NULL, LLM_NORM_RMS, il); -+ cb(cur, "ffn_post_norm", il); -+ -+ cur = ggml_add(ctx0, cur, ffn_inp); -+ cur = build_cvec(cur, il); -+ cb(cur, "l_out", il); -+ -+ inpL = cur; -+ } -+ -+ cur = build_norm(inpL, model.output_norm, NULL, LLM_NORM_RMS, -1); -+ cb(cur, "result_norm", -1); -+ res->t_embd = cur; -+ -+ cur = build_lora_mm(model.output, cur, model.output_s); -+ cb(cur, "result_output", -1); -+ res->t_logits = cur; -+ -+ ggml_build_forward_expand(gf, cur); -+} -diff --git a/src/models/models.h b/src/models/models.h -index 023ed3021..84eff7e97 100644 ---- a/src/models/models.h -+++ b/src/models/models.h -@@ -1972,6 +1972,18 @@ struct llama_model_afmoe : public llama_model_base { - }; - - -+struct llama_model_kolibri1 : public llama_model_base { -+ llama_model_kolibri1(const struct llama_model_params & params) : llama_model_base(params) {} -+ void load_arch_hparams(llama_model_loader & ml) override; -+ void load_arch_tensors(llama_model_loader & ml) override; -+ -+ struct graph : public llm_graph_context { -+ graph(const llama_model & model, const llm_graph_params & params); -+ }; -+ -+ std::unique_ptr build_arch_graph(const llm_graph_params & params) const override; -+}; -+ - struct llama_model_laguna : public llama_model_base { - llama_model_laguna(const struct llama_model_params & params) : llama_model_base(params) {} - void load_arch_hparams(llama_model_loader & ml) override; diff --git a/llama/patches/0019-ggml-cpu-select-the-neon-fp32-to-fp16-conversion.patch b/llama/patches/0019-ggml-cpu-select-the-neon-fp32-to-fp16-conversion.patch deleted file mode 100644 index 1d1618162..000000000 --- a/llama/patches/0019-ggml-cpu-select-the-neon-fp32-to-fp16-conversion.patch +++ /dev/null @@ -1,124 +0,0 @@ -ggml-cpu : select the NEON fp32->fp16 conversion - -The __ARM_NEON branch of ggml/src/ggml-cpu/simd-mappings.h defines both -GGML_CPU_COMPUTE_FP16_TO_FP32 and GGML_CPU_COMPUTE_FP32_TO_FP16 but selects only the first, so -GGML_CPU_FP32_TO_FP16 falls through to GGML_COMPUTE_FP32_TO_FP16 at the end of that section -- -ggml-base's deliberately portable software bit manipulation, the one ggml.c uses everywhere -because ggml-base may not assume CPU features -- while neon_compute_fp32_to_fp16, a one-line -__fp16 conversion, stays defined and referenced by nothing. The whole purpose of the GGML_CPU_* -pair is to be the accelerated variant; on ARM it was defined to be the unaccelerated one. - -__POWER9_VECTOR__ and __riscv_zfhmin select both macros, and the comment on that fallback has -always said NEON does too ("so we define GGML_CPU_FP16_TO_FP32 and GGML_CPU_FP32_TO_FP16 -elsewhere for NEON. This is also true for POWER9."). Not a regression: the NEON selection is -absent in all 25 most recent commits touching the file, back to 2025-04-10. - -Why it is felt on ARM and nowhere else: ggml_cpu_fp32_to_fp16 -- the from_float of GGML_TYPE_F16 --- has vector paths for F16C, RISC-V and s390x (the last added in llama.cpp #30157) and none for -NEON, so there the whole function is the scalar loop through the macro. - -THE GUARD, AND WHY ITS FIRST HALF HAS A CARVE-OUT. In place of the stale fallback comment an -#error pair refuses a branch that defines a COMPUTE macro without selecting it -- the omission -that neither a build, a warning nor a test could see, because the fallback silently supplies a -correct implementation, so the only symptom is a hardware instruction nothing ever reaches. The -FP16->FP32 half carries `&& !defined(__F16C__)`, with the issue link on the condition line, -because llama.cpp #30343 (upstream commit 685f70894) puts that one direction back on the lookup -table on purpose: clang lowers the F16C intrinsic into the live accumulator register of the -quantized dot products and serialises the loop, 45-65 % slower Q8_0/Q4_0 MUL_MAT_ID kernels. -Without the carve-out the guard would fire on every x86 build with F16C -- here that is 11 of the -14 x86-64 CPU variant modules. The FP32->FP16 half needs no exception: F16C, NEON (with this -patch), POWER9 and riscv_zfhmin all select it. Should #30342 ever be resolved and the F16C -selection come back, the carve-out becomes dead weight and should be deleted; that is what the -link on that line is for. - -The guard's position -- immediately before the fallback rather than directly after the #elif -chain -- was chosen by a mutation measurement, not by taste: before the fallback it catches 4 of -4 correctness cases with no false alarms, while directly after the chain it misses one (a COMPUTE -macro defined after the chain and never selected) and, since #30343, fails every x86 F16C build -outright, because that commit leaves the F16C selection below the lookup-table declarations. - -THIS DIFF APPLIES BOTH AT THE PIN AND ON UPSTREAM MASTER, because it no longer touches the F16C -block at all. An earlier version of this patch also moved #30270's two F16C selections into the -__F16C__ branch; that was dropped when #30343 turned the block into a documented exception -carrying an issue link. At the pinned tag #30343 is not in yet, so the F16C block there still -selects both directions and the carve-out is inert -- x86 behaviour at the pin is exactly -upstream's. Once the pin crosses 685f70894 the carve-out becomes load-bearing. - -WHY THIS PROJECT CARRIES IT. All 17 ARM artifacts shipped here -- cpu-linux-aarch64 (8 variants), -cpu-android-aarch64 (7), cpu-windows-aarch64 and metal-macos-aarch64 -- contain no fcvt of any -form in ggml_cpu_fp32_to_fp16, and neither do upstream's own llama-b11576-bin-ubuntu-arm64 and --macos-arm64. A plain gcc with no flags defines __ARM_NEON and __ARM_FP16_FORMAT_IEEE on aarch64, -so there is no opt-in and no way to miss it. - -MEASURED. The function runs 2.45x to 13.3x faster with the selection, across five ARM core types -(Ampere eMAG, Cortex-A55, Neoverse-N1, Neoverse-V2, Apple M1), three operating systems and nine -compiler builds, with the opposite direction measured in the same binary and run as a control that -stays within 3.2 % everywhere. Bit-exact: both paths agree on every one of the 2^32 float32 bit -patterns, differing only in NaN payload -- where the same comparison on x86 F16C gives the same -numbers, so this brings ARM into agreement with what x86 already ships. It is NOT an inference -speed-up: perf puts 83.9 % of an F16 pp512 run in tinyBLAS's gemm, which converts inside its own -microkernel, and 0.03 % in this function. The full record, including the mutation measurement that -chose the guard's position, is docs/benchmarks/arm-fp16-conversion.md. - -Built in upstream's own ARM release configuration (GGML_BACKEND_DL, GGML_CPU_ALL_VARIANTS, -GGML_NATIVE=OFF): all eight variant modules compile, the guard fires nowhere, fcvt/fcvtn/fcvtn2 -appear in every one of them, reverting in the same tree puts that back to zero, and ctest gives -52 of 54 with the two failures identical on unpatched master. Verified again with clang -E against -the pinned source after the rebase: the guard is silent in all five configurations (x86 +F16C+AVX2, -+F16C+AVX512, sse42, plain x86, NEON), and fires when the NEON selection is removed again. - -SUBMITTED UPSTREAM as ggml-org/llama.cpp#30505 (open), rebased onto master and byte-identical to -this diff. - -On a bump, check whether upstream merged it. THE OUTPUT MUST BE READ, NOT TESTED -- run it against -the branch, so that "nothing" is the answer while the patch is needed: - - git show :ggml/src/ggml-cpu/simd-mappings.h \ - | sed -n '/^#if defined(__ARM_NEON)/,/^#elif/p' | grep -n GGML_CPU_FP32_TO_FP16 - -One line printed means upstream merged it; then DROP this patch rather than refreshing it -- the -fail-loud applier detects "does not apply", never "upstream already fixed this". - -Do NOT reduce the looser form -`git grep -n "GGML_CPU_FP32_TO_FP16(x) GGML_CPU_COMPUTE_FP32_TO_FP16" ggml/src/ggml-cpu/simd-mappings.h` -to a true/false: it returns THREE hits at every tag so far -- __POWER9_VECTOR__, __riscv_zfhmin and -the out-of-chain __F16C__ block -- and the first two carry the NEON line's four-space indentation, so -a `grep -q` reports "upstream fixed it" at every tag and deletes a fix that is still needed. -Measured at b11639 (a0e6dc738): hits at lines 71, 114 and 150, none of them inside the __ARM_NEON -branch, while `git apply --check` of this patch is clean -- the two answers contradict each other, -and the grep is the one that is wrong. - -diff --git a/ggml/src/ggml-cpu/simd-mappings.h b/ggml/src/ggml-cpu/simd-mappings.h -index ebb54a4ab..e886347cd 100644 ---- a/ggml/src/ggml-cpu/simd-mappings.h -+++ b/ggml/src/ggml-cpu/simd-mappings.h -@@ -42,6 +42,7 @@ extern "C" { - #define GGML_CPU_COMPUTE_FP32_TO_FP16(x) neon_compute_fp32_to_fp16(x) - - #define GGML_CPU_FP16_TO_FP32(x) GGML_CPU_COMPUTE_FP16_TO_FP32(x) -+ #define GGML_CPU_FP32_TO_FP16(x) GGML_CPU_COMPUTE_FP32_TO_FP16(x) - - static inline float neon_compute_fp16_to_fp32(ggml_fp16_t h) { - __fp16 tmp; -@@ -150,9 +151,18 @@ extern float ggml_table_f32_ue4m3[1 << 8]; - #define GGML_CPU_FP32_TO_FP16(x) GGML_CPU_COMPUTE_FP32_TO_FP16(x) - #endif - --// On ARM NEON, it's quicker to directly convert x -> x instead of calling into ggml_lookup_fp16_to_fp32, --// so we define GGML_CPU_FP16_TO_FP32 and GGML_CPU_FP32_TO_FP16 elsewhere for NEON. --// This is also true for POWER9. -+// A branch above that defines GGML_CPU_COMPUTE_FP16_TO_FP32 / GGML_CPU_COMPUTE_FP32_TO_FP16 must -+// also select it below, or the accelerated conversion stays defined and referenced by nothing while -+// the fallback silently takes over -- the defect #30270 and this change fixed. -+#if defined(GGML_CPU_COMPUTE_FP16_TO_FP32) && !defined(GGML_CPU_FP16_TO_FP32) && !defined(__F16C__) // see issue #30342 -+#error "a branch defined GGML_CPU_COMPUTE_FP16_TO_FP32 without selecting it as GGML_CPU_FP16_TO_FP32" -+#endif -+#if defined(GGML_CPU_COMPUTE_FP32_TO_FP16) && !defined(GGML_CPU_FP32_TO_FP16) -+#error "a branch defined GGML_CPU_COMPUTE_FP32_TO_FP16 without selecting it as GGML_CPU_FP32_TO_FP16" -+#endif -+ -+// Fall back to the lookup table / the portable software conversion for whichever direction -+// no branch above selected. - #if !defined(GGML_CPU_FP16_TO_FP32) - inline static float ggml_lookup_fp16_to_fp32(ggml_fp16_t f) { - uint16_t s; diff --git a/llama/patches/0021-server-clear-the-slot-cache-when-the-server-adapters-change.patch b/llama/patches/0021-server-clear-the-slot-cache-when-the-server-adapters-change.patch new file mode 100644 index 000000000..d161bcee6 --- /dev/null +++ b/llama/patches/0021-server-clear-the-slot-cache-when-the-server-adapters-change.patch @@ -0,0 +1,77 @@ +server : clear a slot's cache when the server's LoRA adapters change + +A slot's prompt cache holds KV entries computed under the LoRA adapters the slot ran with. +launch_slot_with_task() respects that for a request that names its own adapters: when they differ +from the slot's, lora_should_clear_cache() decides, and the cache is cleared unless only aLoRA +adapters are involved. A request that names none takes params_base.lora_adapters in the else +branch -- without that check. So the cache survives two changes of the adapters it was computed +with: + + - SET_LORA (POST /lora-adapters, and the embedding API's set-adapters call) replaces + params_base.lora_adapters, and the next request on a warm slot reuses its prefix; + - a request with its own adapters fills the cache, and the next request without any runs with + the server's adapters on top of it. + +The slot then goes on with KV entries computed under the old scales and answers like neither the +old nor the new adapter set, silently. Measured at b11639 with the pair from the server's own LoRA +test (stories15M_MOE-F16.gguf + moe_shakespeare15M.gguf, greedy, one slot, prompt "Look in thy +glass"): after setting the adapter's scale from 1 to 0, the warm slot answered ". He saw a picture +of a bird ..." where the same model with the adapter at 0 on an emptied slot answers " was a little +girl. She was only three years old ..." -- and erasing the slot was the only way back. + +The patch applies the per-request rule in the else branch too: when params_base.lora_adapters +differ from the slot's, lora_should_clear_cache() decides, the same trace lines are logged, and +the slot takes the server's adapters. Equal sets keep the cache exactly as before. + +For that to hold on a slot's first task, init() starts every slot with the server's adapters. +slot.lora used to start empty, so a never-used slot of a server started with --lora read as +"computed under no adapters": its first task logged "0 loras -> 1 loras" and cleared a prompt +the slot already held -- a state restored into it (SLOT_RESTORE) or loaded from the host-memory +prompt cache by get_available_slot(), both computed under the server's adapters. The same holds +for upstream's per-request branch when a request names the server's own adapters. A SET_LORA +before a slot's first task is still a change and clears, which costs nothing on an empty slot. + +Not covered: the host-memory prompt cache (--cache-ram) stores slot states without the adapters +they were computed with, so a state saved under other adapters can still be loaded into a slot; +that holds for per-request adapters as much as for these. + +The guard is src/test/cpp/test_lora_cache.cpp: a random llama model and a random LoRA adapter on +K, V and the FFN output, greedy, one slot; after each kind of change the warm slot must answer +like an emptied one. A second fixture with two slots restores slot 0's saved state into the +never-used slot 1, which must reuse it. + +Drop-check on every bump: read the else branch of the "process per-request lora adapters" block of +launch_slot_with_task() in tools/server/server-context.cpp at the new tag -- once it compares +params_base.lora_adapters with slot.lora and clears the prompt (or SET_LORA clears the slots), +drop that hunk rather than refreshing it; and read the slot loop of init() -- once it sets +slot.lora from params_base.lora_adapters, drop that hunk. The patch goes when both have. + +diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp +index 22a7805aa..8293b7bfd 100644 +--- a/tools/server/server-context.cpp ++++ b/tools/server/server-context.cpp +@@ -1447,6 +1447,7 @@ + server_slot & slot = slots[i]; + + slot.id = i; ++ slot.lora = params_base.lora_adapters; + slot.ctx_tgt = ctx_tgt; + slot.ctx_dft = ctx_dft; + slot.mem.init(ctx_tgt, ctx_dft); +@@ -1882,7 +1883,15 @@ + } + slot.lora = task_loras; + } +- } else { ++ } else if (!are_lora_equal(params_base.lora_adapters, slot.lora)) { ++ // the server's adapters (changed by SET_LORA, or restored after a request that named its ++ // own) differ from the ones the slot's cache was computed with: same rule as above ++ if (lora_should_clear_cache(slot.lora, params_base.lora_adapters)) { ++ SLT_TRC(slot, "clearing cache for lora change. %zu loras -> %zu loras\n", slot.lora.size(), params_base.lora_adapters.size()); ++ slot.prompt.clear(); ++ } else { ++ SLT_TRC(slot, "keeping cache for alora. %zu target loras\n", params_base.lora_adapters.size()); ++ } + slot.lora = params_base.lora_adapters; + } + diff --git a/llama/pom.xml b/llama/pom.xml index 87eaac199..9051e635f 100644 --- a/llama/pom.xml +++ b/llama/pom.xml @@ -93,7 +93,8 @@ SPDX-License-Identifier: MIT coding agents at all." 1.9.3 is the last pre-disclosure release and is the pinned version for this repo. Any CI / Dependabot / contributor PR that bumps this MUST be rejected. See CLAUDE.md - section "jqwik prompt-injection in test output" for full context. --> + section "jqwik Policy", which points to the cross-repo policy + workspace/policies/jqwik-prompt-injection.md, for full context. --> 1.9.3 1.5.1 4.10.5.0 @@ -585,6 +586,28 @@ SPDX-License-Identifier: MIT maven-resources-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + default-jar + + + + net.ladenthin.llama.server.ServerLauncher + + + + + + org.apache.maven.plugins maven-source-plugin diff --git a/llama/spotbugs-exclude.xml b/llama/spotbugs-exclude.xml index 0514d12f9..7fbc3714b 100644 --- a/llama/spotbugs-exclude.xml +++ b/llama/spotbugs-exclude.xml @@ -137,6 +137,34 @@ SPDX-License-Identifier: MIT + + + + + + + + + + + + + + - + - - - + + + + + + + + + + + + + + + + + @@ -542,8 +602,14 @@ SPDX-License-Identifier: MIT + + + + + +