Skip to content

Commit e7cbc5e

Browse files
committed
llama-langchain4j: model-backed integration tests reusing the shared GGUF cache
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rt1paYztGJ2AKUuBuAGDXE
1 parent 78c382b commit e7cbc5e

5 files changed

Lines changed: 183 additions & 6 deletions

File tree

.github/workflows/publish.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,49 @@ jobs:
132132
- name: Build and test llama-langchain4j
133133
run: mvn -B --no-transfer-progress -f llama-langchain4j/pom.xml verify
134134

135+
# ---------------------------------------------------------------------------
136+
# Model-backed integration for the langchain4j adapters. Reuses the SAME shared GGUF
137+
# cache and the SAME Linux-x86_64 native artifact the core Java jobs already use — no
138+
# extra model download and no duplicated download logic (restore-only cache, no curl
139+
# steps). Runs after test-java-linux-x86_64 so the shared cache is guaranteed populated
140+
# (that job saves it), then exercises the chat / embedding / scoring adapters against
141+
# the already-cached chat (Qwen3-0.6B), nomic-embedding and jina-reranker models. The
142+
# model-backed tests self-skip when a model is absent, so a cold cache degrades to a
143+
# skip, never a failure.
144+
# ---------------------------------------------------------------------------
145+
146+
test-java-llama-langchain4j-integration:
147+
name: Integration Test llama-langchain4j (model-backed)
148+
needs: [crosscompile-linux-x86_64, test-java-linux-x86_64]
149+
runs-on: ubuntu-latest
150+
steps:
151+
- uses: actions/checkout@v7
152+
- name: Download Linux x86_64 native library (reused, not rebuilt)
153+
uses: actions/download-artifact@v8
154+
with:
155+
name: Linux-x86_64-libraries
156+
path: ${{ github.workspace }}/src/main/resources/net/ladenthin/llama/
157+
- name: Restore shared GGUF model cache (restore-only; populated by the Java test jobs)
158+
uses: actions/cache@v6
159+
with:
160+
path: models/
161+
key: gguf-models-v1
162+
- uses: actions/setup-java@v5
163+
with:
164+
distribution: 'temurin'
165+
java-version: ${{ env.JAVA_VERSION }}
166+
- name: Install core net.ladenthin:llama jar (bundles the downloaded native library)
167+
run: >
168+
mvn -B --no-transfer-progress -DskipTests -Denforcer.skip=true
169+
-Dspotless.check.skip=true -Dspotbugs.skip=true
170+
-Dmaven.javadoc.skip=true -Dmaven.source.skip=true -Dgpg.skip=true install
171+
- name: Run llama-langchain4j model-backed integration tests (reused cached models)
172+
run: >
173+
mvn -B --no-transfer-progress -f llama-langchain4j/pom.xml test
174+
-Dnet.ladenthin.llama.model.path=models/${REASONING_MODEL_NAME}
175+
-Dnet.ladenthin.llama.langchain4j.embedding.model=models/${NOMIC_EMBED_MODEL_NAME}
176+
-Dnet.ladenthin.llama.langchain4j.rerank.model=models/${RERANKING_MODEL_NAME}
177+
135178
# ---------------------------------------------------------------------------
136179
# Build the llama.cpp WebUI ONCE, from the same pinned tag CMakeLists.txt fetches,
137180
# and share it to every native build as the generated, platform-independent

CLAUDE.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,12 +1247,19 @@ Wiring:
12471247
2. **`.github/workflows/publish.yml`** — the `test-java-llama-langchain4j` job installs the
12481248
core Java jar, runs a **version-lockstep guard** (module version must equal core version,
12491249
else the build fails — the standalone module can't inherit `${project.version}` from a
1250-
reactor), then `mvn -f llama-langchain4j/pom.xml verify` (mapping unit tests run; the
1251-
model-backed `JllamaChatModelIntegrationTest` self-skips without a GGUF; `verify` also
1252-
builds the javadoc jar so a release-time javadoc break is caught in PR CI). The
1250+
reactor), then `mvn -f llama-langchain4j/pom.xml verify` (7 model-free mapping unit tests
1251+
run; the 4 model-backed integration tests self-skip without a GGUF; `verify` also builds the
1252+
javadoc jar so a release-time javadoc break is caught in PR CI). The
12531253
`publish-snapshot`/`publish-release` jobs `needs:` this job and, after the core `deploy`
12541254
(which installs the core jar locally), run a second `deploy` of the module at the same
1255-
version.
1255+
version. A separate **`test-java-llama-langchain4j-integration`** job runs the model-backed
1256+
tests (chat/streaming/embedding/scoring adapters) by **reusing** the shared GGUF cache
1257+
(`gguf-models-v1`, restore-only — no extra download) and the `Linux-x86_64-libraries` native
1258+
artifact: it runs after `test-java-linux-x86_64` (which populates the cache), installs the
1259+
core jar with the downloaded native lib bundled, and passes the already-cached chat
1260+
(`REASONING_MODEL_NAME`), nomic-embedding and jina-reranker model paths via the module's
1261+
`-Dnet.ladenthin.llama.langchain4j.{embedding,rerank}.model` / `net.ladenthin.llama.model.path`
1262+
properties. It is validation-only (not a release gate); a cold cache degrades to a self-skip.
12561263
3. **Version bumps** — when the root `pom.xml` `<version>` changes, bump
12571264
`llama-langchain4j/pom.xml` `<version>` to match in the same commit, or the lockstep guard
12581265
reds CI.

llama-langchain4j/README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,24 @@ cd llama-langchain4j
8383
mvn test
8484
```
8585

86-
The end-to-end test (`JllamaChatModelIntegrationTest`) self-skips unless you pass a model:
86+
The model-backed integration tests self-skip unless you point them at a GGUF. Each adapter has
87+
its own property so you can run them independently (a chat/instruct model, an embedding-mode model,
88+
and a reranking-mode model respectively):
8789

8890
```bash
89-
mvn test -Dnet.ladenthin.llama.model.path=/abs/path/to/model.gguf
91+
# chat + streaming (JllamaChatModelIntegrationTest)
92+
mvn test -Dnet.ladenthin.llama.model.path=/abs/path/to/chat.gguf
93+
# embeddings (JllamaEmbeddingModelIntegrationTest)
94+
mvn test -Dnet.ladenthin.llama.langchain4j.embedding.model=/abs/path/to/embedding.gguf
95+
# re-ranking / scoring (JllamaScoringModelIntegrationTest)
96+
mvn test -Dnet.ladenthin.llama.langchain4j.rerank.model=/abs/path/to/reranker.gguf
9097
```
9198

99+
In CI these reuse the project's existing shared GGUF cache (the chat, nomic-embedding and
100+
jina-reranker models the core test jobs already download) — the
101+
`test-java-llama-langchain4j-integration` job restores that cache and the
102+
`Linux-x86_64` native library artifact, so no extra model is downloaded.
103+
92104
## Not mapped yet
93105

94106
- **Tool calling.** `ChatRequest.toolSpecifications()` are not forwarded, so the chat adapters return
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
package net.ladenthin.llama.langchain4j;
6+
7+
import static org.hamcrest.MatcherAssert.assertThat;
8+
import static org.hamcrest.Matchers.greaterThan;
9+
import static org.hamcrest.Matchers.is;
10+
import static org.hamcrest.Matchers.notNullValue;
11+
12+
import dev.langchain4j.data.embedding.Embedding;
13+
import dev.langchain4j.data.segment.TextSegment;
14+
import dev.langchain4j.model.output.Response;
15+
import java.nio.file.Files;
16+
import java.nio.file.Path;
17+
import java.nio.file.Paths;
18+
import java.util.Arrays;
19+
import java.util.List;
20+
import net.ladenthin.llama.LlamaModel;
21+
import net.ladenthin.llama.parameters.ModelParameters;
22+
import org.junit.jupiter.api.Assumptions;
23+
import org.junit.jupiter.api.Test;
24+
25+
/**
26+
* End-to-end smoke test for {@link JllamaEmbeddingModel} over a real embedding model. Self-skips
27+
* unless {@code -Dnet.ladenthin.llama.langchain4j.embedding.model=/abs/path/to/embedding.gguf}
28+
* points at a GGUF loadable in embedding mode (and the native library is present), mirroring the
29+
* core project's model-gated tests. CI reuses the already-cached nomic embedding model, so no extra
30+
* download is introduced.
31+
*/
32+
class JllamaEmbeddingModelIntegrationTest {
33+
34+
private static Path modelPath() {
35+
String path = System.getProperty("net.ladenthin.llama.langchain4j.embedding.model");
36+
Assumptions.assumeTrue(path != null && !path.isEmpty(), "embedding model path property not set");
37+
Path resolved = Paths.get(path);
38+
Assumptions.assumeTrue(Files.exists(resolved), "embedding model file not present: " + resolved);
39+
return resolved;
40+
}
41+
42+
@Test
43+
void embedsAllSegmentsInInputOrder() {
44+
Path model = modelPath();
45+
try (LlamaModel llama =
46+
new LlamaModel(new ModelParameters().setModel(model.toString()).enableEmbedding())) {
47+
JllamaEmbeddingModel embeddingModel = new JllamaEmbeddingModel(llama);
48+
49+
List<TextSegment> segments =
50+
Arrays.asList(TextSegment.from("hello world"), TextSegment.from("goodbye world"));
51+
Response<List<Embedding>> response = embeddingModel.embedAll(segments);
52+
53+
assertThat(response, is(notNullValue()));
54+
assertThat(response.content().size(), is(2));
55+
assertThat(response.content().get(0).vector().length, is(greaterThan(0)));
56+
}
57+
}
58+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
package net.ladenthin.llama.langchain4j;
6+
7+
import static org.hamcrest.MatcherAssert.assertThat;
8+
import static org.hamcrest.Matchers.is;
9+
import static org.hamcrest.Matchers.notNullValue;
10+
11+
import dev.langchain4j.data.segment.TextSegment;
12+
import dev.langchain4j.model.output.Response;
13+
import java.nio.file.Files;
14+
import java.nio.file.Path;
15+
import java.nio.file.Paths;
16+
import java.util.Arrays;
17+
import java.util.List;
18+
import net.ladenthin.llama.LlamaModel;
19+
import net.ladenthin.llama.parameters.ModelParameters;
20+
import org.junit.jupiter.api.Assumptions;
21+
import org.junit.jupiter.api.Test;
22+
23+
/**
24+
* End-to-end smoke test for {@link JllamaScoringModel} (re-ranker) over a real reranking model.
25+
* Self-skips unless {@code -Dnet.ladenthin.llama.langchain4j.rerank.model=/abs/path/to/reranker.gguf}
26+
* points at a GGUF loadable in reranking mode (and the native library is present), mirroring the
27+
* core project's model-gated tests. CI reuses the already-cached jina reranker model, so no extra
28+
* download is introduced.
29+
*/
30+
class JllamaScoringModelIntegrationTest {
31+
32+
private static Path modelPath() {
33+
String path = System.getProperty("net.ladenthin.llama.langchain4j.rerank.model");
34+
Assumptions.assumeTrue(path != null && !path.isEmpty(), "rerank model path property not set");
35+
Path resolved = Paths.get(path);
36+
Assumptions.assumeTrue(Files.exists(resolved), "rerank model file not present: " + resolved);
37+
return resolved;
38+
}
39+
40+
@Test
41+
void scoresEverySegmentInInputOrder() {
42+
Path model = modelPath();
43+
try (LlamaModel llama =
44+
new LlamaModel(new ModelParameters().setModel(model.toString()).enableReranking())) {
45+
JllamaScoringModel scoringModel = new JllamaScoringModel(llama);
46+
47+
List<TextSegment> segments =
48+
Arrays.asList(
49+
TextSegment.from("A cat sat on the mat."),
50+
TextSegment.from("The stock market fell today."));
51+
Response<List<Double>> response = scoringModel.scoreAll(segments, "domestic pets");
52+
53+
assertThat(response, is(notNullValue()));
54+
assertThat(response.content().size(), is(2));
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)