diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 72003ec2..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 # --------------------------------------------------------------------------- @@ -93,6 +143,87 @@ 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 + + # --------------------------------------------------------------------------- + # Model-backed integration for the langchain4j adapters. Reuses the SAME shared GGUF + # 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, download-models] + 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 (populated by download-models; no re-download) + 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 @@ -908,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 @@ -924,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 @@ -1051,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 @@ -1067,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 @@ -1140,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 @@ -1156,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 @@ -1229,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 @@ -1245,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 @@ -1318,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 @@ -1337,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 @@ -1427,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 @@ -1446,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 @@ -1665,7 +1656,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 +1722,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 +1773,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 +1829,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..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). @@ -1219,6 +1225,56 @@ 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` (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. 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 `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. +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 80% rename from langchain4j-jllama/README.md rename to llama-langchain4j/README.md index cb9d99bd..1082d973 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,16 +79,28 @@ build here: mvn -DskipTests install # then build/test this module -cd langchain4j-jllama +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/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/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)); + } + } +} 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