From 78c382b9cbfc375247bf23ebb7d5f6530b9ca3d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 11:14:11 +0000 Subject: [PATCH 1/3] Rename langchain4j module to llama-langchain4j; wire CI build/test + Central publish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleans up the integration of the merged langchain4j adapters (PR #284) so the module is built, gated, version-locked and releasable — without touching the native build/release pipeline. - Rename artifact + directory langchain4j-jllama -> llama-langchain4j so it groups with the core net.ladenthin:llama family (Java package unchanged). - Pin the core dependency to ${project.version} (drops the drift-prone jllama.version property); a CI guard fails the build if the module version ever diverges from the core version (standalone module can't inherit it from a reactor). - Add per-artifact release plumbing (sources + javadoc + gpg + Central Publishing) mirroring the core release profile, so the module can deploy to Maven Central at the same version. - publish.yml: new test-java-llama-langchain4j job (install core Java jar, version-lockstep guard, mvn verify — builds the javadoc jar so a release-time javadoc break is caught in PR CI). publish-snapshot/publish-release now depend on it and deploy the module alongside the core. - REUSE.toml + README updated to the new name; CLAUDE.md documents the module, why it is a separate artifact (not a classifier), and the CI/publish wiring. Verified locally: core Java jar installs, module builds green (7 mapping tests pass, 2 model-backed integration tests self-skip), and the main/sources/javadoc jars all build under doclint=all. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Rt1paYztGJ2AKUuBuAGDXE --- .github/workflows/publish.yml | 67 +++++- CLAUDE.md | 42 ++++ REUSE.toml | 2 +- langchain4j-jllama/pom.xml | 94 --------- .../README.md | 6 +- llama-langchain4j/pom.xml | 193 ++++++++++++++++++ .../llama/langchain4j/JllamaChatModel.java | 0 .../langchain4j/JllamaEmbeddingModel.java | 0 .../llama/langchain4j/JllamaScoringModel.java | 0 .../langchain4j/JllamaStreamingChatModel.java | 0 .../llama/langchain4j/LangChain4jMapping.java | 0 .../llama/langchain4j/package-info.java | 0 .../JllamaChatModelIntegrationTest.java | 0 .../langchain4j/LangChain4jMappingTest.java | 0 14 files changed, 304 insertions(+), 100 deletions(-) delete mode 100644 langchain4j-jllama/pom.xml rename {langchain4j-jllama => llama-langchain4j}/README.md (98%) create mode 100644 llama-langchain4j/pom.xml rename {langchain4j-jllama => llama-langchain4j}/src/main/java/net/ladenthin/llama/langchain4j/JllamaChatModel.java (100%) rename {langchain4j-jllama => llama-langchain4j}/src/main/java/net/ladenthin/llama/langchain4j/JllamaEmbeddingModel.java (100%) rename {langchain4j-jllama => llama-langchain4j}/src/main/java/net/ladenthin/llama/langchain4j/JllamaScoringModel.java (100%) rename {langchain4j-jllama => llama-langchain4j}/src/main/java/net/ladenthin/llama/langchain4j/JllamaStreamingChatModel.java (100%) rename {langchain4j-jllama => llama-langchain4j}/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java (100%) rename {langchain4j-jllama => llama-langchain4j}/src/main/java/net/ladenthin/llama/langchain4j/package-info.java (100%) rename {langchain4j-jllama => llama-langchain4j}/src/test/java/net/ladenthin/llama/langchain4j/JllamaChatModelIntegrationTest.java (100%) rename {langchain4j-jllama => llama-langchain4j}/src/test/java/net/ladenthin/llama/langchain4j/LangChain4jMappingTest.java (100%) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 72003ec2..1b8e3401 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -93,6 +93,45 @@ jobs: echo "=== internal package dependency graph (jdeps, bytecode) ===" jdeps -verbose:package target/classes | grep 'net.ladenthin.llama' || true + # --------------------------------------------------------------------------- + # Sibling module `llama-langchain4j` (LangChain4j adapters). Pure Java, no native + # code and no per-classifier matrix: it compiles against the core's stable Java API + # (identical across every classifier) and the backend is a runtime choice for the + # consumer. This job installs the core Java jar, guards that the module version is in + # lockstep with the core, then builds + tests the module (Java 17; langchain4j 1.x + # baseline). It runs its mapping unit tests; the model-backed integration test + # self-skips without a GGUF. `verify` also builds the javadoc/sources jars so a + # release-time javadoc break is caught here in PR CI. + # --------------------------------------------------------------------------- + + test-java-llama-langchain4j: + name: Build and Test llama-langchain4j + needs: startgate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v5 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: temurin + - name: Version lockstep guard (module version must equal core version) + shell: bash + run: | + CORE=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version | tail -n1) + MOD=$(mvn -q -DforceStdout -f llama-langchain4j/pom.xml help:evaluate -Dexpression=project.version | tail -n1) + echo "core=$CORE module=$MOD" + if [ "$CORE" != "$MOD" ]; then + echo "::error::Version drift: core ($CORE) != llama-langchain4j ($MOD). Keep llama-langchain4j/pom.xml in lockstep with the root pom.xml." + exit 1 + fi + - name: Install core net.ladenthin:llama jar (Java only) + run: > + mvn -B --no-transfer-progress -DskipTests -Denforcer.skip=true + -Dspotless.check.skip=true -Dspotbugs.skip=true + -Dmaven.javadoc.skip=true -Dmaven.source.skip=true -Dgpg.skip=true install + - name: Build and test llama-langchain4j + run: mvn -B --no-transfer-progress -f llama-langchain4j/pom.xml verify + # --------------------------------------------------------------------------- # Build the llama.cpp WebUI ONCE, from the same pinned tag CMakeLists.txt fetches, # and share it to every native build as the generated, platform-independent @@ -1665,7 +1704,7 @@ jobs: publish-snapshot: name: Publish Snapshot to Central - needs: [check-snapshot, crosscompile-linux-x86_64-cuda, crosscompile-android-aarch64-opencl, code-style] + needs: [check-snapshot, crosscompile-linux-x86_64-cuda, crosscompile-android-aarch64-opencl, code-style, test-java-llama-langchain4j] if: needs.check-snapshot.result == 'success' && inputs.publish_to_central runs-on: ubuntu-latest environment: maven-central @@ -1731,11 +1770,23 @@ jobs: MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }} MAVEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN }} MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + # Deploy the sibling llama-langchain4j at the same version. The core `deploy` + # above ran through `install`, so net.ladenthin:llama is in the local repo for + # the module to resolve. Standalone module (not in the reactor), so it is a + # separate deploy invocation. + - name: Publish snapshot (llama-langchain4j) + run: mvn --batch-mode --no-transfer-progress -f llama-langchain4j/pom.xml -P release -Dmaven.test.skip=true deploy + env: + MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - name: Collect signed artifacts run: | mkdir -p signed-snapshot-assets cp target/*.jar signed-snapshot-assets/ 2>/dev/null || true cp target/*.jar.asc signed-snapshot-assets/ 2>/dev/null || true + cp llama-langchain4j/target/*.jar signed-snapshot-assets/ 2>/dev/null || true + cp llama-langchain4j/target/*.jar.asc signed-snapshot-assets/ 2>/dev/null || true - uses: actions/upload-artifact@v7 with: name: signed-snapshot-assets @@ -1770,7 +1821,7 @@ jobs: publish-release: name: Publish Release to Central if: needs.check-tag.result == 'success' && inputs.publish_to_central - needs: [check-tag, crosscompile-linux-x86_64-cuda, crosscompile-android-aarch64-opencl, code-style] + needs: [check-tag, crosscompile-linux-x86_64-cuda, crosscompile-android-aarch64-opencl, code-style, test-java-llama-langchain4j] runs-on: ubuntu-latest environment: maven-central permissions: @@ -1826,11 +1877,23 @@ jobs: MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }} MAVEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN }} MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + # Deploy the sibling llama-langchain4j at the same version. The core `deploy` + # above ran through `install`, so net.ladenthin:llama is in the local repo for + # the module to resolve. Standalone module (not in the reactor), so it is a + # separate deploy invocation. + - name: Publish release (llama-langchain4j) + run: mvn --batch-mode --no-transfer-progress -f llama-langchain4j/pom.xml -P release -Dmaven.test.skip=true deploy + env: + MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - name: Collect signed artifacts run: | mkdir -p signed-release-assets cp target/*.jar signed-release-assets/ 2>/dev/null || true cp target/*.jar.asc signed-release-assets/ 2>/dev/null || true + cp llama-langchain4j/target/*.jar signed-release-assets/ 2>/dev/null || true + cp llama-langchain4j/target/*.jar.asc signed-release-assets/ 2>/dev/null || true - uses: actions/upload-artifact@v7 with: name: signed-release-assets diff --git a/CLAUDE.md b/CLAUDE.md index 7139beef..bc6d86ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1219,6 +1219,48 @@ keeping it clear of the JPMS module-mode javadoc trap that bit BAF. **Before rai javadoc source level to ≥ 9, read** [`../workspace/policies/jpms-module-descriptor.md`](../workspace/policies/jpms-module-descriptor.md). +## LangChain4j integration (`llama-langchain4j` sibling module) + +`llama-langchain4j/` adapts a `LlamaModel` to LangChain4j's `ChatModel`, +`StreamingChatModel`, `EmbeddingModel` and `ScoringModel` interfaces **in-process over +JNI** (no HTTP hop). It is a **standalone sibling module**, deliberately *not* in the root +reactor, so the native build/release pipeline is untouched. + +Why it is a **separate artifact** and not a classifier of the core: langchain4j 1.x +requires **Java 17** (the core stays Java 8), and classifiers share the core's single POM — +adding `langchain4j-core` there would force it (and the Java 17 floor) on every plain +`net.ladenthin:llama` consumer. A separate `artifactId` with its own POM is the only way to +keep that dependency (and Java floor) off the core. It is pure Java with **no per-classifier +matrix**: it compiles against the core's Java API, which is identical across every native +classifier; the backend (CPU/CUDA/OpenCL/Vulkan) is a runtime classpath choice for the +consumer. + +Wiring: + +1. **`llama-langchain4j/pom.xml`** — `net.ladenthin:llama-langchain4j`, `release 17`, + depends on `net.ladenthin:llama:${project.version}` (so the core dep always matches the + module's own version) and `dev.langchain4j:langchain4j-core`. Carries its own + sources/javadoc/gpg + `release` profile (Central requires per-artifact signing; the module + has no parent to inherit them from — plugin versions are pinned in lockstep with the root + `pom.xml`). Java package stays `net.ladenthin.llama.langchain4j` (package name need not track + the artifactId). +2. **`.github/workflows/publish.yml`** — the `test-java-llama-langchain4j` job installs the + core Java jar, runs a **version-lockstep guard** (module version must equal core version, + else the build fails — the standalone module can't inherit `${project.version}` from a + reactor), then `mvn -f llama-langchain4j/pom.xml verify` (mapping unit tests run; the + model-backed `JllamaChatModelIntegrationTest` self-skips without a GGUF; `verify` also + builds the javadoc jar so a release-time javadoc break is caught in PR CI). The + `publish-snapshot`/`publish-release` jobs `needs:` this job and, after the core `deploy` + (which installs the core jar locally), run a second `deploy` of the module at the same + version. +3. **Version bumps** — when the root `pom.xml` `` changes, bump + `llama-langchain4j/pom.xml` `` to match in the same commit, or the lockstep guard + reds CI. + +**Open follow-ups** (documented in `llama-langchain4j/README.md`): tool calling +(`ToolSpecification` ↔ jllama `ToolDefinition`), `response_format`/JSON mode, and multimodal +user input (currently flattened to text). + ## Open TODOs Open TODOs for this repo live in [`TODO.md`](TODO.md). Cross-repo status diff --git a/REUSE.toml b/REUSE.toml index 54d18d08..df4c5422 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -24,7 +24,7 @@ path = [ ".github/ISSUE_TEMPLATE/bug_report.md", ".github/ISSUE_TEMPLATE/feature_request.md", ".claude/commands/find-cpp-duplication.md", - "langchain4j-jllama/README.md", + "llama-langchain4j/README.md", ] SPDX-FileCopyrightText = [ "2023-2025 Konstantin Herud", diff --git a/langchain4j-jllama/pom.xml b/langchain4j-jllama/pom.xml deleted file mode 100644 index 58f6d365..00000000 --- a/langchain4j-jllama/pom.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - 4.0.0 - - net.ladenthin - langchain4j-jllama - 5.0.4-SNAPSHOT - jar - - ${project.groupId}:${project.artifactId} - LangChain4j integration for java-llama.cpp: in-process ChatModel, - StreamingChatModel, EmbeddingModel and ScoringModel adapters backed by a - llama.cpp model over JNI (no HTTP hop). - https://github.com/bernardladenthin/java-llama.cpp - - - - MIT License - https://www.opensource.org/licenses/mit-license.php - repo - - - - - - Bernard Ladenthin - https://github.com/bernardladenthin - - - - - scm:git:https://github.com/bernardladenthin/java-llama.cpp.git - scm:git:https://github.com/bernardladenthin/java-llama.cpp.git - https://github.com/bernardladenthin/java-llama.cpp/tree/main - - - - UTF-8 - 17 - - 5.0.4-SNAPSHOT - 1.17.1 - 6.1.1 - 3.0 - 3.5.5 - - - - - - net.ladenthin - llama - ${jllama.version} - - - - - dev.langchain4j - langchain4j-core - ${langchain4j.version} - - - - org.junit.jupiter - junit-jupiter - ${junit.version} - test - - - org.hamcrest - hamcrest - ${hamcrest.version} - test - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - ${surefire.version} - - - - diff --git a/langchain4j-jllama/README.md b/llama-langchain4j/README.md similarity index 98% rename from langchain4j-jllama/README.md rename to llama-langchain4j/README.md index cb9d99bd..5fc5fd3a 100644 --- a/langchain4j-jllama/README.md +++ b/llama-langchain4j/README.md @@ -1,4 +1,4 @@ -# langchain4j-jllama +# llama-langchain4j [LangChain4j](https://github.com/langchain4j/langchain4j) adapters backed by an **in-process** [java-llama.cpp](https://github.com/bernardladenthin/java-llama.cpp) model over JNI — no HTTP server, @@ -61,7 +61,7 @@ ScoringModel reranker = new JllamaScoringModel(rerankLlama); ```xml net.ladenthin - langchain4j-jllama + llama-langchain4j 5.0.4-SNAPSHOT ``` @@ -79,7 +79,7 @@ build here: mvn -DskipTests install # then build/test this module -cd langchain4j-jllama +cd llama-langchain4j mvn test ``` diff --git a/llama-langchain4j/pom.xml b/llama-langchain4j/pom.xml new file mode 100644 index 00000000..ecb0b8bb --- /dev/null +++ b/llama-langchain4j/pom.xml @@ -0,0 +1,193 @@ + + + + 4.0.0 + + net.ladenthin + llama-langchain4j + 5.0.4-SNAPSHOT + jar + + ${project.groupId}:${project.artifactId} + LangChain4j integration for java-llama.cpp: in-process ChatModel, + StreamingChatModel, EmbeddingModel and ScoringModel adapters backed by a + llama.cpp model over JNI (no HTTP hop). + https://github.com/bernardladenthin/java-llama.cpp + + + + MIT License + https://www.opensource.org/licenses/mit-license.php + repo + + + + + + Bernard Ladenthin + https://github.com/bernardladenthin + + + + + scm:git:https://github.com/bernardladenthin/java-llama.cpp.git + scm:git:https://github.com/bernardladenthin/java-llama.cpp.git + https://github.com/bernardladenthin/java-llama.cpp/tree/main + + + + + central + https://central.sonatype.com/repository/maven-snapshots/ + + + + + UTF-8 + + 17 + 1.17.1 + 6.1.1 + 3.0 + + 3.15.0 + 3.5.6 + 3.4.0 + 3.12.0 + 3.2.8 + 0.11.0 + + + + + + net.ladenthin + llama + ${project.version} + + + + + dev.langchain4j + langchain4j-core + ${langchain4j.version} + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.hamcrest + hamcrest + ${hamcrest.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${compiler.plugin.version} + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + + + + org.apache.maven.plugins + maven-source-plugin + ${source.plugin.version} + + + attach-sources + + jar-no-fork + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${javadoc.plugin.version} + + 17 + true + all + + + + attach-javadocs + + jar + + + + + + + + + + + release + + + + org.apache.maven.plugins + maven-gpg-plugin + ${gpg.plugin.version} + + + sign-artifacts + verify + sign + + ${gpg.keyname} + + --pinentry-mode + loopback + + + + + + + + org.sonatype.central + central-publishing-maven-plugin + ${central.plugin.version} + true + + central + true + published + + + + + + + diff --git a/langchain4j-jllama/src/main/java/net/ladenthin/llama/langchain4j/JllamaChatModel.java b/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/JllamaChatModel.java similarity index 100% rename from langchain4j-jllama/src/main/java/net/ladenthin/llama/langchain4j/JllamaChatModel.java rename to llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/JllamaChatModel.java diff --git a/langchain4j-jllama/src/main/java/net/ladenthin/llama/langchain4j/JllamaEmbeddingModel.java b/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/JllamaEmbeddingModel.java similarity index 100% rename from langchain4j-jllama/src/main/java/net/ladenthin/llama/langchain4j/JllamaEmbeddingModel.java rename to llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/JllamaEmbeddingModel.java diff --git a/langchain4j-jllama/src/main/java/net/ladenthin/llama/langchain4j/JllamaScoringModel.java b/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/JllamaScoringModel.java similarity index 100% rename from langchain4j-jllama/src/main/java/net/ladenthin/llama/langchain4j/JllamaScoringModel.java rename to llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/JllamaScoringModel.java diff --git a/langchain4j-jllama/src/main/java/net/ladenthin/llama/langchain4j/JllamaStreamingChatModel.java b/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/JllamaStreamingChatModel.java similarity index 100% rename from langchain4j-jllama/src/main/java/net/ladenthin/llama/langchain4j/JllamaStreamingChatModel.java rename to llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/JllamaStreamingChatModel.java diff --git a/langchain4j-jllama/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java b/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java similarity index 100% rename from langchain4j-jllama/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java rename to llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/LangChain4jMapping.java diff --git a/langchain4j-jllama/src/main/java/net/ladenthin/llama/langchain4j/package-info.java b/llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/package-info.java similarity index 100% rename from langchain4j-jllama/src/main/java/net/ladenthin/llama/langchain4j/package-info.java rename to llama-langchain4j/src/main/java/net/ladenthin/llama/langchain4j/package-info.java diff --git a/langchain4j-jllama/src/test/java/net/ladenthin/llama/langchain4j/JllamaChatModelIntegrationTest.java b/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/JllamaChatModelIntegrationTest.java similarity index 100% rename from langchain4j-jllama/src/test/java/net/ladenthin/llama/langchain4j/JllamaChatModelIntegrationTest.java rename to llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/JllamaChatModelIntegrationTest.java diff --git a/langchain4j-jllama/src/test/java/net/ladenthin/llama/langchain4j/LangChain4jMappingTest.java b/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/LangChain4jMappingTest.java similarity index 100% rename from langchain4j-jllama/src/test/java/net/ladenthin/llama/langchain4j/LangChain4jMappingTest.java rename to llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/LangChain4jMappingTest.java From e7cbc5e2487d067bf0c7b52d09f4f7b1a5892c86 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 11:35:24 +0000 Subject: [PATCH 2/3] llama-langchain4j: model-backed integration tests reusing the shared GGUF cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds end-to-end coverage for the embedding and scoring adapters (previously only chat had an integration test) and wires a CI job that exercises all four adapters against the models the pipeline already caches — no new model, no duplicate download logic. - New JllamaEmbeddingModelIntegrationTest / JllamaScoringModelIntegrationTest, self-skipping via -Dnet.ladenthin.llama.langchain4j.embedding.model and .rerank.model (mirrors the existing chat integration test). Module now: 7 mapping tests + 4 model-backed integration tests (self-skip without a GGUF). - New test-java-llama-langchain4j-integration job reuses the existing shared cache (gguf-models-v1, restore-only) and the Linux-x86_64-libraries native artifact. It runs after test-java-linux-x86_64 (which populates the cache), installs the core jar with the bundled native lib, and points the adapters at the already-cached chat (Qwen3-0.6B), nomic-embedding and jina-reranker models. Validation-only (not a release gate); a cold cache degrades to a self-skip. - README + CLAUDE.md document the per-adapter model properties and the cache reuse. Verified locally: module builds green, 7 mapping tests pass, 4 integration tests self-skip without models. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Rt1paYztGJ2AKUuBuAGDXE --- .github/workflows/publish.yml | 43 ++++++++++++++ CLAUDE.md | 15 +++-- llama-langchain4j/README.md | 16 ++++- .../JllamaEmbeddingModelIntegrationTest.java | 58 +++++++++++++++++++ .../JllamaScoringModelIntegrationTest.java | 57 ++++++++++++++++++ 5 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/JllamaEmbeddingModelIntegrationTest.java create mode 100644 llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/JllamaScoringModelIntegrationTest.java diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1b8e3401..d7f548a3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -132,6 +132,49 @@ jobs: - name: Build and test llama-langchain4j run: mvn -B --no-transfer-progress -f llama-langchain4j/pom.xml verify + # --------------------------------------------------------------------------- + # Model-backed integration for the langchain4j adapters. Reuses the SAME shared GGUF + # cache and the SAME Linux-x86_64 native artifact the core Java jobs already use — no + # extra model download and no duplicated download logic (restore-only cache, no curl + # steps). Runs after test-java-linux-x86_64 so the shared cache is guaranteed populated + # (that job saves it), then exercises the chat / embedding / scoring adapters against + # the already-cached chat (Qwen3-0.6B), nomic-embedding and jina-reranker models. The + # model-backed tests self-skip when a model is absent, so a cold cache degrades to a + # skip, never a failure. + # --------------------------------------------------------------------------- + + test-java-llama-langchain4j-integration: + name: Integration Test llama-langchain4j (model-backed) + needs: [crosscompile-linux-x86_64, test-java-linux-x86_64] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Download Linux x86_64 native library (reused, not rebuilt) + uses: actions/download-artifact@v8 + with: + name: Linux-x86_64-libraries + path: ${{ github.workspace }}/src/main/resources/net/ladenthin/llama/ + - name: Restore shared GGUF model cache (restore-only; populated by the Java test jobs) + uses: actions/cache@v6 + with: + path: models/ + key: gguf-models-v1 + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + - name: Install core net.ladenthin:llama jar (bundles the downloaded native library) + run: > + mvn -B --no-transfer-progress -DskipTests -Denforcer.skip=true + -Dspotless.check.skip=true -Dspotbugs.skip=true + -Dmaven.javadoc.skip=true -Dmaven.source.skip=true -Dgpg.skip=true install + - name: Run llama-langchain4j model-backed integration tests (reused cached models) + run: > + mvn -B --no-transfer-progress -f llama-langchain4j/pom.xml test + -Dnet.ladenthin.llama.model.path=models/${REASONING_MODEL_NAME} + -Dnet.ladenthin.llama.langchain4j.embedding.model=models/${NOMIC_EMBED_MODEL_NAME} + -Dnet.ladenthin.llama.langchain4j.rerank.model=models/${RERANKING_MODEL_NAME} + # --------------------------------------------------------------------------- # Build the llama.cpp WebUI ONCE, from the same pinned tag CMakeLists.txt fetches, # and share it to every native build as the generated, platform-independent diff --git a/CLAUDE.md b/CLAUDE.md index bc6d86ca..d42bf89f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1247,12 +1247,19 @@ Wiring: 2. **`.github/workflows/publish.yml`** — the `test-java-llama-langchain4j` job installs the core Java jar, runs a **version-lockstep guard** (module version must equal core version, else the build fails — the standalone module can't inherit `${project.version}` from a - reactor), then `mvn -f llama-langchain4j/pom.xml verify` (mapping unit tests run; the - model-backed `JllamaChatModelIntegrationTest` self-skips without a GGUF; `verify` also - builds the javadoc jar so a release-time javadoc break is caught in PR CI). The + reactor), then `mvn -f llama-langchain4j/pom.xml verify` (7 model-free mapping unit tests + run; the 4 model-backed integration tests self-skip without a GGUF; `verify` also builds the + javadoc jar so a release-time javadoc break is caught in PR CI). The `publish-snapshot`/`publish-release` jobs `needs:` this job and, after the core `deploy` (which installs the core jar locally), run a second `deploy` of the module at the same - version. + version. A separate **`test-java-llama-langchain4j-integration`** job runs the model-backed + tests (chat/streaming/embedding/scoring adapters) by **reusing** the shared GGUF cache + (`gguf-models-v1`, restore-only — no extra download) and the `Linux-x86_64-libraries` native + artifact: it runs after `test-java-linux-x86_64` (which populates the cache), installs the + core jar with the downloaded native lib bundled, and passes the already-cached chat + (`REASONING_MODEL_NAME`), nomic-embedding and jina-reranker model paths via the module's + `-Dnet.ladenthin.llama.langchain4j.{embedding,rerank}.model` / `net.ladenthin.llama.model.path` + properties. It is validation-only (not a release gate); a cold cache degrades to a self-skip. 3. **Version bumps** — when the root `pom.xml` `` changes, bump `llama-langchain4j/pom.xml` `` to match in the same commit, or the lockstep guard reds CI. diff --git a/llama-langchain4j/README.md b/llama-langchain4j/README.md index 5fc5fd3a..1082d973 100644 --- a/llama-langchain4j/README.md +++ b/llama-langchain4j/README.md @@ -83,12 +83,24 @@ cd llama-langchain4j mvn test ``` -The end-to-end test (`JllamaChatModelIntegrationTest`) self-skips unless you pass a model: +The model-backed integration tests self-skip unless you point them at a GGUF. Each adapter has +its own property so you can run them independently (a chat/instruct model, an embedding-mode model, +and a reranking-mode model respectively): ```bash -mvn test -Dnet.ladenthin.llama.model.path=/abs/path/to/model.gguf +# chat + streaming (JllamaChatModelIntegrationTest) +mvn test -Dnet.ladenthin.llama.model.path=/abs/path/to/chat.gguf +# embeddings (JllamaEmbeddingModelIntegrationTest) +mvn test -Dnet.ladenthin.llama.langchain4j.embedding.model=/abs/path/to/embedding.gguf +# re-ranking / scoring (JllamaScoringModelIntegrationTest) +mvn test -Dnet.ladenthin.llama.langchain4j.rerank.model=/abs/path/to/reranker.gguf ``` +In CI these reuse the project's existing shared GGUF cache (the chat, nomic-embedding and +jina-reranker models the core test jobs already download) — the +`test-java-llama-langchain4j-integration` job restores that cache and the +`Linux-x86_64` native library artifact, so no extra model is downloaded. + ## Not mapped yet - **Tool calling.** `ChatRequest.toolSpecifications()` are not forwarded, so the chat adapters return diff --git a/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/JllamaEmbeddingModelIntegrationTest.java b/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/JllamaEmbeddingModelIntegrationTest.java new file mode 100644 index 00000000..79e0384a --- /dev/null +++ b/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/JllamaEmbeddingModelIntegrationTest.java @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2026 Bernard Ladenthin +// +// SPDX-License-Identifier: MIT + +package net.ladenthin.llama.langchain4j; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.output.Response; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import net.ladenthin.llama.LlamaModel; +import net.ladenthin.llama.parameters.ModelParameters; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +/** + * End-to-end smoke test for {@link JllamaEmbeddingModel} over a real embedding model. Self-skips + * unless {@code -Dnet.ladenthin.llama.langchain4j.embedding.model=/abs/path/to/embedding.gguf} + * points at a GGUF loadable in embedding mode (and the native library is present), mirroring the + * core project's model-gated tests. CI reuses the already-cached nomic embedding model, so no extra + * download is introduced. + */ +class JllamaEmbeddingModelIntegrationTest { + + private static Path modelPath() { + String path = System.getProperty("net.ladenthin.llama.langchain4j.embedding.model"); + Assumptions.assumeTrue(path != null && !path.isEmpty(), "embedding model path property not set"); + Path resolved = Paths.get(path); + Assumptions.assumeTrue(Files.exists(resolved), "embedding model file not present: " + resolved); + return resolved; + } + + @Test + void embedsAllSegmentsInInputOrder() { + Path model = modelPath(); + try (LlamaModel llama = + new LlamaModel(new ModelParameters().setModel(model.toString()).enableEmbedding())) { + JllamaEmbeddingModel embeddingModel = new JllamaEmbeddingModel(llama); + + List segments = + Arrays.asList(TextSegment.from("hello world"), TextSegment.from("goodbye world")); + Response> response = embeddingModel.embedAll(segments); + + assertThat(response, is(notNullValue())); + assertThat(response.content().size(), is(2)); + assertThat(response.content().get(0).vector().length, is(greaterThan(0))); + } + } +} diff --git a/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/JllamaScoringModelIntegrationTest.java b/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/JllamaScoringModelIntegrationTest.java new file mode 100644 index 00000000..4c61c16d --- /dev/null +++ b/llama-langchain4j/src/test/java/net/ladenthin/llama/langchain4j/JllamaScoringModelIntegrationTest.java @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2026 Bernard Ladenthin +// +// SPDX-License-Identifier: MIT + +package net.ladenthin.llama.langchain4j; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.output.Response; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import net.ladenthin.llama.LlamaModel; +import net.ladenthin.llama.parameters.ModelParameters; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +/** + * End-to-end smoke test for {@link JllamaScoringModel} (re-ranker) over a real reranking model. + * Self-skips unless {@code -Dnet.ladenthin.llama.langchain4j.rerank.model=/abs/path/to/reranker.gguf} + * points at a GGUF loadable in reranking mode (and the native library is present), mirroring the + * core project's model-gated tests. CI reuses the already-cached jina reranker model, so no extra + * download is introduced. + */ +class JllamaScoringModelIntegrationTest { + + private static Path modelPath() { + String path = System.getProperty("net.ladenthin.llama.langchain4j.rerank.model"); + Assumptions.assumeTrue(path != null && !path.isEmpty(), "rerank model path property not set"); + Path resolved = Paths.get(path); + Assumptions.assumeTrue(Files.exists(resolved), "rerank model file not present: " + resolved); + return resolved; + } + + @Test + void scoresEverySegmentInInputOrder() { + Path model = modelPath(); + try (LlamaModel llama = + new LlamaModel(new ModelParameters().setModel(model.toString()).enableReranking())) { + JllamaScoringModel scoringModel = new JllamaScoringModel(llama); + + List segments = + Arrays.asList( + TextSegment.from("A cat sat on the mat."), + TextSegment.from("The stock market fell today.")); + Response> response = scoringModel.scoreAll(segments, "domestic pets"); + + assertThat(response, is(notNullValue())); + assertThat(response.content().size(), is(2)); + } + } +} From c2ed8c8f45acaf0aefe633d1b9a0447984725a6a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 11:46:08 +0000 Subject: [PATCH 3/3] ci: download GGUF models once upfront, restore-only everywhere (DRY the pipeline) Replaces the per-job model download logic (duplicated across all 6 Java test jobs) with a single upfront download-models job; every test job now only restores the shared cache. - New download-models job (needs: startgate) is the single source of the download logic: the ten curl steps + validate-models.sh live only there. It restores the shared cache (key gguf-models-v1) or, on a cold cache, downloads the full ~5 GB set, validates, and saves it. - All 6 Java test jobs (Linux, 3x macOS, 2x Windows) drop their duplicated download blocks and now needs: download-models + restore-only the cache, keeping validate-models.{sh,bat} as a per-job integrity guard. Removes the cold-start save race; net -91 lines in publish.yml. - The llama-langchain4j integration job now needs [crosscompile-linux-x86_64, download-models] instead of chaining behind test-java-linux-x86_64, so it runs in parallel off the guaranteed-populated cache. - CLAUDE.md updated: the model-cache section documents the upfront download-models job (replacing the old "save race" description) and the integration job wiring. YAML validated; structural counts verified (10 curl steps only in download-models; 7 jobs restore the cache; 7 validate steps). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Rt1paYztGJ2AKUuBuAGDXE --- .github/workflows/publish.yml | 287 ++++++++++++---------------------- CLAUDE.md | 23 ++- 2 files changed, 113 insertions(+), 197 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d7f548a3..b4ec2ad5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -68,6 +68,56 @@ jobs: steps: - run: echo "Start gate elapsed — proceeding with pipeline." + # --------------------------------------------------------------------------- + # Download + cache the GGUF test models ONCE, upfront, for the whole pipeline. + # Every Java test job (`test-java-*`) and the langchain4j integration job `needs:` this + # job and then only RESTORES the shared cache (key gguf-models-v1) — so the ~5 GB model + # set is fetched from HuggingFace at most once per cache lifetime instead of racing to + # download in each job. GGUF is platform-independent, so this single ubuntu job's cache + # is reused by the macOS and Windows jobs too. On a warm cache this job is a no-op + # restore; on a cold cache it downloads all models, validates them, and saves the cache + # at job end (immutable key, so downstream jobs restore-hit). This is the single source + # of the download logic — do not re-add per-job downloads. + # --------------------------------------------------------------------------- + + download-models: + name: Download + cache GGUF models (once) + needs: startgate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Cache GGUF models (GitHub Actions cache; avoids re-downloading from HuggingFace) + uses: actions/cache@v6 + with: + path: models/ + # GGUF is platform-independent, so ubuntu + macOS + Windows share one entry; + # bump the suffix when the model set / URLs change. + key: gguf-models-v1 + - name: Download text generation model + run: test -f models/${MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${MODEL_URL} --create-dirs -o models/${MODEL_NAME} + - name: Download reranking model + run: test -f models/${RERANKING_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${RERANKING_MODEL_URL} --create-dirs -o models/${RERANKING_MODEL_NAME} + - name: Download draft model + run: test -f models/${DRAFT_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${DRAFT_MODEL_URL} --create-dirs -o models/${DRAFT_MODEL_NAME} + - name: Download reasoning model + run: test -f models/${REASONING_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${REASONING_MODEL_URL} --create-dirs -o models/${REASONING_MODEL_NAME} + - name: Download tool-calling model + run: test -f models/${TOOL_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TOOL_MODEL_URL} --create-dirs -o models/${TOOL_MODEL_NAME} + - name: Download nomic embedding model (issue #98 regression) + run: test -f models/${NOMIC_EMBED_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${NOMIC_EMBED_MODEL_URL} --create-dirs -o models/${NOMIC_EMBED_MODEL_NAME} + - name: Download vision model + run: test -f models/${VISION_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${VISION_MODEL_URL} --create-dirs -o models/${VISION_MODEL_NAME} + - name: Download vision mmproj + run: test -f models/${VISION_MMPROJ_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${VISION_MMPROJ_URL} --create-dirs -o models/${VISION_MMPROJ_NAME} + - name: Download TTS model (OuteTTS) + run: test -f models/${TTS_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TTS_MODEL_URL} --create-dirs -o models/${TTS_MODEL_NAME} + - name: Download TTS vocoder (WavTokenizer) + run: test -f models/${TTS_VOCODER_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TTS_VOCODER_URL} --create-dirs -o models/${TTS_VOCODER_NAME} + - name: List files in models directory + run: ls -l models/ + - name: Validate model files + run: bash .github/validate-models.sh + # --------------------------------------------------------------------------- # Cross-compile jobs (Docker / dockcross) — produce release artifacts, no testing # --------------------------------------------------------------------------- @@ -134,18 +184,17 @@ jobs: # --------------------------------------------------------------------------- # Model-backed integration for the langchain4j adapters. Reuses the SAME shared GGUF - # cache and the SAME Linux-x86_64 native artifact the core Java jobs already use — no - # extra model download and no duplicated download logic (restore-only cache, no curl - # steps). Runs after test-java-linux-x86_64 so the shared cache is guaranteed populated - # (that job saves it), then exercises the chat / embedding / scoring adapters against - # the already-cached chat (Qwen3-0.6B), nomic-embedding and jina-reranker models. The - # model-backed tests self-skip when a model is absent, so a cold cache degrades to a - # skip, never a failure. + # cache (populated once by download-models) and the SAME Linux-x86_64 native artifact the + # core Java jobs already use — no extra model download and no duplicated download logic + # (restore-only cache, no curl steps). It exercises the chat / embedding / scoring + # adapters against the already-cached chat (Qwen3-0.6B), nomic-embedding and jina-reranker + # models. The model-backed tests self-skip when a model is absent, so a cold cache degrades + # to a skip, never a failure. # --------------------------------------------------------------------------- test-java-llama-langchain4j-integration: name: Integration Test llama-langchain4j (model-backed) - needs: [crosscompile-linux-x86_64, test-java-linux-x86_64] + needs: [crosscompile-linux-x86_64, download-models] runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -154,7 +203,7 @@ jobs: with: name: Linux-x86_64-libraries path: ${{ github.workspace }}/src/main/resources/net/ladenthin/llama/ - - name: Restore shared GGUF model cache (restore-only; populated by the Java test jobs) + - name: Restore shared GGUF model cache (populated by download-models; no re-download) uses: actions/cache@v6 with: path: models/ @@ -990,7 +1039,7 @@ jobs: test-java-linux-x86_64: name: Java Tests Ubuntu Latest x86_64 - needs: crosscompile-linux-x86_64 + needs: [crosscompile-linux-x86_64, download-models] runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -1006,41 +1055,16 @@ jobs: with: name: Linux-x86_64-libraries path: ${{ github.workspace }}/src/main/resources/net/ladenthin/llama/ - # GGUF model cache — introduced to stop re-downloading ~5 GB of test models from - # HuggingFace on every run (also dodges HF rate-limits). Complements the sccache compiler - # cache but is always ON: there is intentionally NO on/off flag for it (it is GitHub's - # free cache, safe + free), whereas the sccache cache is toggled by the `use_cache` - # workflow_dispatch input / USE_CACHE env. Not Depot — GB-scale blobs are usage-priced - # there and its file cache needs Depot-hosted runners. See CLAUDE.md. - - name: Cache GGUF models (GitHub Actions cache; avoids re-downloading from HuggingFace) + # GGUF models are downloaded + cached ONCE by the upstream `download-models` job + # (this job `needs:` it), so here we only RESTORE the shared cache — no per-job + # download logic. GGUF is platform-independent, so ubuntu + macOS + Windows share + # one entry (key gguf-models-v1). validate-models is kept as an integrity guard so a + # partial/absent restore fails loudly instead of silently self-skipping required tests. + - name: Restore shared GGUF model cache (populated by download-models; no re-download) uses: actions/cache@v6 with: path: models/ - # GGUF is platform-independent, so ubuntu + macOS + Windows share one entry; - # bump the suffix when the model set / URLs change. key: gguf-models-v1 - - name: Download text generation model - run: test -f models/${MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${MODEL_URL} --create-dirs -o models/${MODEL_NAME} - - name: Download reranking model - run: test -f models/${RERANKING_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${RERANKING_MODEL_URL} --create-dirs -o models/${RERANKING_MODEL_NAME} - - name: Download draft model - run: test -f models/${DRAFT_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${DRAFT_MODEL_URL} --create-dirs -o models/${DRAFT_MODEL_NAME} - - name: Download reasoning model - run: test -f models/${REASONING_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${REASONING_MODEL_URL} --create-dirs -o models/${REASONING_MODEL_NAME} - - name: Download tool-calling model - run: test -f models/${TOOL_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TOOL_MODEL_URL} --create-dirs -o models/${TOOL_MODEL_NAME} - - name: Download nomic embedding model (issue #98 regression) - run: test -f models/${NOMIC_EMBED_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${NOMIC_EMBED_MODEL_URL} --create-dirs -o models/${NOMIC_EMBED_MODEL_NAME} - - name: Download vision model - run: test -f models/${VISION_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${VISION_MODEL_URL} --create-dirs -o models/${VISION_MODEL_NAME} - - name: Download vision mmproj - run: test -f models/${VISION_MMPROJ_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${VISION_MMPROJ_URL} --create-dirs -o models/${VISION_MMPROJ_NAME} - - name: Download TTS model (OuteTTS) - run: test -f models/${TTS_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TTS_MODEL_URL} --create-dirs -o models/${TTS_MODEL_NAME} - - name: Download TTS vocoder (WavTokenizer) - run: test -f models/${TTS_VOCODER_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TTS_VOCODER_URL} --create-dirs -o models/${TTS_VOCODER_NAME} - - name: List files in models directory - run: ls -l models/ - name: Validate model files run: bash .github/validate-models.sh - uses: actions/setup-java@v5 @@ -1133,7 +1157,7 @@ jobs: test-java-macos-arm64-metal: name: Java Tests macOS 14 arm64 (Metal) - needs: build-macos-arm64-metal + needs: [build-macos-arm64-metal, download-models] runs-on: macos-14 steps: - uses: actions/checkout@v7 @@ -1149,41 +1173,16 @@ jobs: with: name: macos-14-libraries path: ${{ github.workspace }}/src/main/resources/net/ladenthin/llama/ - # GGUF model cache — introduced to stop re-downloading ~5 GB of test models from - # HuggingFace on every run (also dodges HF rate-limits). Complements the sccache compiler - # cache but is always ON: there is intentionally NO on/off flag for it (it is GitHub's - # free cache, safe + free), whereas the sccache cache is toggled by the `use_cache` - # workflow_dispatch input / USE_CACHE env. Not Depot — GB-scale blobs are usage-priced - # there and its file cache needs Depot-hosted runners. See CLAUDE.md. - - name: Cache GGUF models (GitHub Actions cache; avoids re-downloading from HuggingFace) + # GGUF models are downloaded + cached ONCE by the upstream `download-models` job + # (this job `needs:` it), so here we only RESTORE the shared cache — no per-job + # download logic. GGUF is platform-independent, so ubuntu + macOS + Windows share + # one entry (key gguf-models-v1). validate-models is kept as an integrity guard so a + # partial/absent restore fails loudly instead of silently self-skipping required tests. + - name: Restore shared GGUF model cache (populated by download-models; no re-download) uses: actions/cache@v6 with: path: models/ - # GGUF is platform-independent, so ubuntu + macOS + Windows share one entry; - # bump the suffix when the model set / URLs change. key: gguf-models-v1 - - name: Download text generation model - run: test -f models/${MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${MODEL_URL} --create-dirs -o models/${MODEL_NAME} - - name: Download reranking model - run: test -f models/${RERANKING_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${RERANKING_MODEL_URL} --create-dirs -o models/${RERANKING_MODEL_NAME} - - name: Download draft model - run: test -f models/${DRAFT_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${DRAFT_MODEL_URL} --create-dirs -o models/${DRAFT_MODEL_NAME} - - name: Download reasoning model - run: test -f models/${REASONING_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${REASONING_MODEL_URL} --create-dirs -o models/${REASONING_MODEL_NAME} - - name: Download tool-calling model - run: test -f models/${TOOL_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TOOL_MODEL_URL} --create-dirs -o models/${TOOL_MODEL_NAME} - - name: Download nomic embedding model (issue #98 regression) - run: test -f models/${NOMIC_EMBED_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${NOMIC_EMBED_MODEL_URL} --create-dirs -o models/${NOMIC_EMBED_MODEL_NAME} - - name: Download vision model - run: test -f models/${VISION_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${VISION_MODEL_URL} --create-dirs -o models/${VISION_MODEL_NAME} - - name: Download vision mmproj - run: test -f models/${VISION_MMPROJ_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${VISION_MMPROJ_URL} --create-dirs -o models/${VISION_MMPROJ_NAME} - - name: Download TTS model (OuteTTS) - run: test -f models/${TTS_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TTS_MODEL_URL} --create-dirs -o models/${TTS_MODEL_NAME} - - name: Download TTS vocoder (WavTokenizer) - run: test -f models/${TTS_VOCODER_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TTS_VOCODER_URL} --create-dirs -o models/${TTS_VOCODER_NAME} - - name: List files in models directory - run: ls -l models/ - name: Validate model files run: bash .github/validate-models.sh - uses: actions/setup-java@v5 @@ -1222,7 +1221,7 @@ jobs: test-java-macos-arm64-no-metal: name: Java Tests macOS 15 arm64 (no Metal) - needs: build-macos-arm64-no-metal + needs: [build-macos-arm64-no-metal, download-models] runs-on: macos-15 steps: - uses: actions/checkout@v7 @@ -1238,41 +1237,16 @@ jobs: with: name: macos-15-libraries path: ${{ github.workspace }}/src/main/resources/net/ladenthin/llama/ - # GGUF model cache — introduced to stop re-downloading ~5 GB of test models from - # HuggingFace on every run (also dodges HF rate-limits). Complements the sccache compiler - # cache but is always ON: there is intentionally NO on/off flag for it (it is GitHub's - # free cache, safe + free), whereas the sccache cache is toggled by the `use_cache` - # workflow_dispatch input / USE_CACHE env. Not Depot — GB-scale blobs are usage-priced - # there and its file cache needs Depot-hosted runners. See CLAUDE.md. - - name: Cache GGUF models (GitHub Actions cache; avoids re-downloading from HuggingFace) + # GGUF models are downloaded + cached ONCE by the upstream `download-models` job + # (this job `needs:` it), so here we only RESTORE the shared cache — no per-job + # download logic. GGUF is platform-independent, so ubuntu + macOS + Windows share + # one entry (key gguf-models-v1). validate-models is kept as an integrity guard so a + # partial/absent restore fails loudly instead of silently self-skipping required tests. + - name: Restore shared GGUF model cache (populated by download-models; no re-download) uses: actions/cache@v6 with: path: models/ - # GGUF is platform-independent, so ubuntu + macOS + Windows share one entry; - # bump the suffix when the model set / URLs change. key: gguf-models-v1 - - name: Download text generation model - run: test -f models/${MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${MODEL_URL} --create-dirs -o models/${MODEL_NAME} - - name: Download reranking model - run: test -f models/${RERANKING_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${RERANKING_MODEL_URL} --create-dirs -o models/${RERANKING_MODEL_NAME} - - name: Download draft model - run: test -f models/${DRAFT_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${DRAFT_MODEL_URL} --create-dirs -o models/${DRAFT_MODEL_NAME} - - name: Download reasoning model - run: test -f models/${REASONING_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${REASONING_MODEL_URL} --create-dirs -o models/${REASONING_MODEL_NAME} - - name: Download tool-calling model - run: test -f models/${TOOL_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TOOL_MODEL_URL} --create-dirs -o models/${TOOL_MODEL_NAME} - - name: Download nomic embedding model (issue #98 regression) - run: test -f models/${NOMIC_EMBED_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${NOMIC_EMBED_MODEL_URL} --create-dirs -o models/${NOMIC_EMBED_MODEL_NAME} - - name: Download vision model - run: test -f models/${VISION_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${VISION_MODEL_URL} --create-dirs -o models/${VISION_MODEL_NAME} - - name: Download vision mmproj - run: test -f models/${VISION_MMPROJ_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${VISION_MMPROJ_URL} --create-dirs -o models/${VISION_MMPROJ_NAME} - - name: Download TTS model (OuteTTS) - run: test -f models/${TTS_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TTS_MODEL_URL} --create-dirs -o models/${TTS_MODEL_NAME} - - name: Download TTS vocoder (WavTokenizer) - run: test -f models/${TTS_VOCODER_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TTS_VOCODER_URL} --create-dirs -o models/${TTS_VOCODER_NAME} - - name: List files in models directory - run: ls -l models/ - name: Validate model files run: bash .github/validate-models.sh - uses: actions/setup-java@v5 @@ -1311,7 +1285,7 @@ jobs: test-java-macos-arm64-metal-15: name: Java Tests macOS 15 arm64 (Metal) - needs: build-macos-arm64-metal-15 + needs: [build-macos-arm64-metal-15, download-models] runs-on: macos-15 steps: - uses: actions/checkout@v7 @@ -1327,41 +1301,16 @@ jobs: with: name: macos-15-metal-libraries path: ${{ github.workspace }}/src/main/resources/net/ladenthin/llama/ - # GGUF model cache — introduced to stop re-downloading ~5 GB of test models from - # HuggingFace on every run (also dodges HF rate-limits). Complements the sccache compiler - # cache but is always ON: there is intentionally NO on/off flag for it (it is GitHub's - # free cache, safe + free), whereas the sccache cache is toggled by the `use_cache` - # workflow_dispatch input / USE_CACHE env. Not Depot — GB-scale blobs are usage-priced - # there and its file cache needs Depot-hosted runners. See CLAUDE.md. - - name: Cache GGUF models (GitHub Actions cache; avoids re-downloading from HuggingFace) + # GGUF models are downloaded + cached ONCE by the upstream `download-models` job + # (this job `needs:` it), so here we only RESTORE the shared cache — no per-job + # download logic. GGUF is platform-independent, so ubuntu + macOS + Windows share + # one entry (key gguf-models-v1). validate-models is kept as an integrity guard so a + # partial/absent restore fails loudly instead of silently self-skipping required tests. + - name: Restore shared GGUF model cache (populated by download-models; no re-download) uses: actions/cache@v6 with: path: models/ - # GGUF is platform-independent, so ubuntu + macOS + Windows share one entry; - # bump the suffix when the model set / URLs change. key: gguf-models-v1 - - name: Download text generation model - run: test -f models/${MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${MODEL_URL} --create-dirs -o models/${MODEL_NAME} - - name: Download reranking model - run: test -f models/${RERANKING_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${RERANKING_MODEL_URL} --create-dirs -o models/${RERANKING_MODEL_NAME} - - name: Download draft model - run: test -f models/${DRAFT_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${DRAFT_MODEL_URL} --create-dirs -o models/${DRAFT_MODEL_NAME} - - name: Download reasoning model - run: test -f models/${REASONING_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${REASONING_MODEL_URL} --create-dirs -o models/${REASONING_MODEL_NAME} - - name: Download tool-calling model - run: test -f models/${TOOL_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TOOL_MODEL_URL} --create-dirs -o models/${TOOL_MODEL_NAME} - - name: Download nomic embedding model (issue #98 regression) - run: test -f models/${NOMIC_EMBED_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${NOMIC_EMBED_MODEL_URL} --create-dirs -o models/${NOMIC_EMBED_MODEL_NAME} - - name: Download vision model - run: test -f models/${VISION_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${VISION_MODEL_URL} --create-dirs -o models/${VISION_MODEL_NAME} - - name: Download vision mmproj - run: test -f models/${VISION_MMPROJ_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${VISION_MMPROJ_URL} --create-dirs -o models/${VISION_MMPROJ_NAME} - - name: Download TTS model (OuteTTS) - run: test -f models/${TTS_MODEL_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TTS_MODEL_URL} --create-dirs -o models/${TTS_MODEL_NAME} - - name: Download TTS vocoder (WavTokenizer) - run: test -f models/${TTS_VOCODER_NAME} || curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors ${TTS_VOCODER_URL} --create-dirs -o models/${TTS_VOCODER_NAME} - - name: List files in models directory - run: ls -l models/ - name: Validate model files run: bash .github/validate-models.sh - uses: actions/setup-java@v5 @@ -1400,7 +1349,7 @@ jobs: test-java-windows-x86_64: name: Java Tests Windows 2025 x86_64 (default / Ninja) - needs: build-windows-x86_64 + needs: [build-windows-x86_64, download-models] runs-on: windows-2025-vs2026 steps: - uses: actions/checkout@v7 @@ -1419,35 +1368,15 @@ jobs: with: name: Windows-x86_64-libraries path: ${{ github.workspace }}/src/main/resources/net/ladenthin/llama/ - - name: Cache GGUF models (GitHub Actions cache; avoids re-downloading from HuggingFace) + # GGUF models are downloaded + cached ONCE by the upstream `download-models` job + # (this job `needs:` it), so here we only RESTORE the shared cache — no per-job + # download logic. validate-models is kept as an integrity guard so a partial/absent + # restore fails loudly instead of silently self-skipping required tests. + - name: Restore shared GGUF model cache (populated by download-models; no re-download) uses: actions/cache@v6 with: path: models/ - # GGUF is platform-independent, so ubuntu + macOS + Windows share one entry; - # bump the suffix when the model set / URLs change. key: gguf-models-v1 - - name: Download text generation model - run: if (-not (Test-Path "models/$env:MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:MODEL_URL --create-dirs -o models/$env:MODEL_NAME } - - name: Download reranking model - run: if (-not (Test-Path "models/$env:RERANKING_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:RERANKING_MODEL_URL --create-dirs -o models/$env:RERANKING_MODEL_NAME } - - name: Download draft model - run: if (-not (Test-Path "models/$env:DRAFT_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:DRAFT_MODEL_URL --create-dirs -o models/$env:DRAFT_MODEL_NAME } - - name: Download reasoning model - run: if (-not (Test-Path "models/$env:REASONING_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:REASONING_MODEL_URL --create-dirs -o models/$env:REASONING_MODEL_NAME } - - name: Download tool-calling model - run: if (-not (Test-Path "models/$env:TOOL_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:TOOL_MODEL_URL --create-dirs -o models/$env:TOOL_MODEL_NAME } - - name: Download nomic embedding model (issue #98 regression) - run: if (-not (Test-Path "models/$env:NOMIC_EMBED_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:NOMIC_EMBED_MODEL_URL --create-dirs -o models/$env:NOMIC_EMBED_MODEL_NAME } - - name: Download vision model - run: if (-not (Test-Path "models/$env:VISION_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:VISION_MODEL_URL --create-dirs -o models/$env:VISION_MODEL_NAME } - - name: Download vision mmproj - run: if (-not (Test-Path "models/$env:VISION_MMPROJ_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:VISION_MMPROJ_URL --create-dirs -o models/$env:VISION_MMPROJ_NAME } - - name: Download TTS model (OuteTTS) - run: if (-not (Test-Path "models/$env:TTS_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:TTS_MODEL_URL --create-dirs -o models/$env:TTS_MODEL_NAME } - - name: Download TTS vocoder (WavTokenizer) - run: if (-not (Test-Path "models/$env:TTS_VOCODER_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:TTS_VOCODER_URL --create-dirs -o models/$env:TTS_VOCODER_NAME } - - name: List files in models directory - run: ls -l models/ - name: Validate model files run: .github\validate-models.bat - uses: actions/setup-java@v5 @@ -1509,7 +1438,7 @@ jobs: # validated end-to-end before the `msvc-windows` classifier JAR ships. test-java-windows-x86_64-msvc: name: Java Tests Windows 2025 x86_64 (MSVC classifier) - needs: build-windows-x86_64-msvc + needs: [build-windows-x86_64-msvc, download-models] runs-on: windows-2025-vs2026 steps: - uses: actions/checkout@v7 @@ -1528,35 +1457,15 @@ jobs: with: name: Windows-x86_64-msvc path: ${{ github.workspace }}/src/main/resources/net/ladenthin/llama/ - - name: Cache GGUF models (GitHub Actions cache; avoids re-downloading from HuggingFace) + # GGUF models are downloaded + cached ONCE by the upstream `download-models` job + # (this job `needs:` it), so here we only RESTORE the shared cache — no per-job + # download logic. validate-models is kept as an integrity guard so a partial/absent + # restore fails loudly instead of silently self-skipping required tests. + - name: Restore shared GGUF model cache (populated by download-models; no re-download) uses: actions/cache@v6 with: path: models/ - # GGUF is platform-independent, so ubuntu + macOS + Windows share one entry; - # bump the suffix when the model set / URLs change. key: gguf-models-v1 - - name: Download text generation model - run: if (-not (Test-Path "models/$env:MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:MODEL_URL --create-dirs -o models/$env:MODEL_NAME } - - name: Download reranking model - run: if (-not (Test-Path "models/$env:RERANKING_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:RERANKING_MODEL_URL --create-dirs -o models/$env:RERANKING_MODEL_NAME } - - name: Download draft model - run: if (-not (Test-Path "models/$env:DRAFT_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:DRAFT_MODEL_URL --create-dirs -o models/$env:DRAFT_MODEL_NAME } - - name: Download reasoning model - run: if (-not (Test-Path "models/$env:REASONING_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:REASONING_MODEL_URL --create-dirs -o models/$env:REASONING_MODEL_NAME } - - name: Download tool-calling model - run: if (-not (Test-Path "models/$env:TOOL_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:TOOL_MODEL_URL --create-dirs -o models/$env:TOOL_MODEL_NAME } - - name: Download nomic embedding model (issue #98 regression) - run: if (-not (Test-Path "models/$env:NOMIC_EMBED_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:NOMIC_EMBED_MODEL_URL --create-dirs -o models/$env:NOMIC_EMBED_MODEL_NAME } - - name: Download vision model - run: if (-not (Test-Path "models/$env:VISION_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:VISION_MODEL_URL --create-dirs -o models/$env:VISION_MODEL_NAME } - - name: Download vision mmproj - run: if (-not (Test-Path "models/$env:VISION_MMPROJ_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:VISION_MMPROJ_URL --create-dirs -o models/$env:VISION_MMPROJ_NAME } - - name: Download TTS model (OuteTTS) - run: if (-not (Test-Path "models/$env:TTS_MODEL_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:TTS_MODEL_URL --create-dirs -o models/$env:TTS_MODEL_NAME } - - name: Download TTS vocoder (WavTokenizer) - run: if (-not (Test-Path "models/$env:TTS_VOCODER_NAME")) { curl -L --proto =https --proto-redir =https --fail --retry 5 --retry-all-errors $env:TTS_VOCODER_URL --create-dirs -o models/$env:TTS_VOCODER_NAME } - - name: List files in models directory - run: ls -l models/ - name: Validate model files run: .github\validate-models.bat - uses: actions/setup-java@v5 diff --git a/CLAUDE.md b/CLAUDE.md index d42bf89f..db03010f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -971,12 +971,18 @@ properties set, so `LlamaEmbeddingsTest`, `MultimodalIntegrationTest`, and `TtsI these as **required** (a missing model hard-fails the job before tests run, so a download regression can never silently downgrade to a skip). The only model still self-skipping is the audio-input model (`AudioInputIntegrationTest`) — it has no committed clip and no CI download. -The shared GGUF cache (`actions/cache`, key `gguf-models-v1`, path `models/`) holds the full set; -since every test job downloads the full set before the cache can save, whichever job wins the -save race caches everything. Because the cache key is immutable, changing the model set means the -**existing cache entry must be deleted** (not bumped to `v2`) so the next run rebuilds it complete -— locally the model tests still self-skip when a GGUF is absent (`Assume.assumeTrue`), so a -partial local checkout is fine. +The shared GGUF cache (`actions/cache`, key `gguf-models-v1`, path `models/`) holds the full set +and is populated **once, upfront** by a dedicated **`download-models`** job (`needs: startgate`): +it is the single place the ~5 GB set is fetched from HuggingFace (the ten `curl` steps + the +`validate-models.sh` gate live only there). Every `test-java-*` job — and the langchain4j +integration job — `needs: download-models` and then only **restores** that cache (no per-job +download, no cold-start save race), keeping `validate-models.{sh,bat}` as a per-job integrity +guard. GGUF is platform-independent, so the one ubuntu `download-models` cache is reused by the +macOS and Windows jobs too. `validate-models.{sh,bat}` treats the models as **required** (a +missing model hard-fails the job before tests run). Because the cache key is immutable, changing +the model set means the **existing cache entry must be deleted** (not bumped to `v2`) so +`download-models` rebuilds it complete — locally the model tests still self-skip when a GGUF is +absent (`Assume.assumeTrue`), so a partial local checkout is fine. Set the model path via system property or environment variable (see test files for exact property names). @@ -1255,8 +1261,9 @@ Wiring: version. A separate **`test-java-llama-langchain4j-integration`** job runs the model-backed tests (chat/streaming/embedding/scoring adapters) by **reusing** the shared GGUF cache (`gguf-models-v1`, restore-only — no extra download) and the `Linux-x86_64-libraries` native - artifact: it runs after `test-java-linux-x86_64` (which populates the cache), installs the - core jar with the downloaded native lib bundled, and passes the already-cached chat + artifact: it `needs: [crosscompile-linux-x86_64, download-models]` (so the cache is already + populated and it runs in parallel), installs the core jar with the downloaded native lib + bundled, and passes the already-cached chat (`REASONING_MODEL_NAME`), nomic-embedding and jina-reranker model paths via the module's `-Dnet.ladenthin.llama.langchain4j.{embedding,rerank}.model` / `net.ladenthin.llama.model.path` properties. It is validation-only (not a release gate); a cold cache degrades to a self-skip.