Self-correction pass caught this before publishing: the previous commit's test
comment and companion post draft claimed a keepAlive set through
ChatClient.prompt().options(...) never reaches the request Ollama receives,
and worked around it by calling ChatModel.call(Prompt) directly instead. That
claim was never actually verified against /api/ps for the ChatClient path --
only inferred from a failed timing assertion that, it turned out, would have
failed the same way even with a genuinely confirmed unload (see below).
Checked directly: unloading via ChatClient.prompt().options(OllamaChatOptions
.builder()...keepAlive("0")).call() and immediately querying /api/ps shows an
empty model registry, same as the ChatModel path. The options merge works
correctly. Simplified the test back to ChatClient throughout, consistent with
the rest of this series, and removed the incorrect comment.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FtpJvZfg4nvLvtzgJTDWpB
ollama-local
Companion code for Run LLMs Locally with Spring AI and Ollama, part of the Spring AI series on ankurm.com.
spring-ai-starter-model-ollama autoconfigures a ChatModel and an EmbeddingModel from spring.ai.ollama.* properties alone -- no API key anywhere in this module. Every test in LocalChatAndEmbeddingTest.java drives a real, local Ollama server started by Testcontainers, answering with a real small model (qwen2.5:0.5b for chat, all-minilm for embeddings) -- unlike every other module in this series, nothing here is scripted.
Versions
| Component | Version |
|---|---|
| Spring Boot | 4.1.1 |
| Spring AI | 2.0.1 |
| Testcontainers | 2.0.5 |
| Java | 25 (LTS) |
org.testcontainers:ollama was renamed to org.testcontainers:testcontainers-ollama in the Testcontainers 2.x line, and org.testcontainers:junit-jupiter to org.testcontainers:testcontainers-junit-jupiter -- both confirmed by reading the real testcontainers-bom-2.0.5.pom that Spring Boot 4.1.1 imports. If you're copying an older Testcontainers-Ollama tutorial, the old artifact IDs still exist on Maven Central but are stuck on the 1.x line and are not what this BOM resolves.
Quickstart
./scripts/bake-image.sh # once: pulls qwen2.5:0.5b + all-minilm into a local image
./scripts/run-all.sh # every time: runs the suite against that baked image
bake-image.sh is the real Testcontainers-recommended pattern for CI: pull the models into a container once, docker commit the result, and every subsequent test run starts a container that already has them on disk -- no registry pull, no network dependency, no per-run latency for the pull itself.
What's here
| File | What it shows |
|---|---|
OllamaLocalApplication.java |
The whole application: no manual OllamaApi/OllamaChatModel wiring, just the starter's autoconfiguration |
config/ChatClientConfig.java |
Wraps the autoconfigured ChatModel in a ChatClient, same as every other module in this series |
application.yml |
The five spring.ai.ollama.* properties this module uses, with pull-model-strategy: never so the app fails fast instead of silently pulling gigabytes at startup |
LocalChatAndEmbeddingTest.java |
A Testcontainers-managed OllamaContainer, a confirmed model unload via keep_alive: 0 + /api/ps, real chat calls, and a deterministic embedding-dimension assertion |
Output files
| File | Captured from |
|---|---|
output/01-chat-reload-after-unload.txt |
reloadAfterAConfirmedUnload -- unloads the model, confirms via /api/ps, then reloads it |
output/02-chat-back-to-back-call.txt |
backToBackCallReusesTheAlreadyLoadedModel |
output/03-embedding-dimensions.txt |
embeddingsAreDeterministicallySized |
Requirements
JDK 25, Maven, Docker. Run scripts/bake-image.sh before the test suite -- without a baked image present, scripts/run-all.sh refuses to start rather than silently pulling ~450MB of models over the network on every test run.