Some call AI on the JVM quixotic.
We call it Quixotic AI.
Finally, AI in a jar.
Every component of the AI stack, a la carte.
jbang header + imports
///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 25+
//RUNTIME_OPTIONS --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED
//DEPS com.qxotic:jinfer-bom:0.2.0@pom
//DEPS com.qxotic:jinfer-spring-ai com.qxotic:jinfer-models-all
//DEPS com.qxotic:jam-native com.qxotic:jam-vector
//DEPS org.slf4j:slf4j-nop:2.0.17
import com.qxotic.jinfer.spring.ai.*;
void main() {
try (var model = JinferChatModel.builder()
.model("LiquidAI/LFM2.5-350M-GGUF:Q8_0")
.build()) {
System.out.println(model.call("What is the capital of France?"));
}
}$ jbang Chat.java # "The capital of France is Paris."
jbang header + imports
///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 25+
//RUNTIME_OPTIONS --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED
//DEPS com.qxotic:jinfer-bom:0.2.0@pom
//DEPS com.qxotic:jinfer-spring-ai
//DEPS com.qxotic:jinfer-kokoro
//DEPS com.qxotic:jam-native com.qxotic:jam-vector
//DEPS org.slf4j:slf4j-nop:2.0.17
import com.qxotic.jinfer.spring.ai.JinferSpeechModel;
import java.nio.file.*;
void main() throws Exception {
try (var tts = JinferSpeechModel.builder()
.model("simonfxr/kokoro.cpp-GGUF:Q8_0")
.companion("voice", "simonfxr/kokoro.cpp-GGUF/voices/kokoro-voice-af_heart.gguf")
.build()) {
var line = "Turns out, matrix multiplications can talk. "
+ "The JVM just crunched a few billion numbers to say this.";
Files.write(Path.of("kokoro.wav"), tts.call(line));
}
}$ jbang TextToSpeech.java # kokoro.wav · seven seconds, in a warm voice · Kokoro-82M
jbang header + imports
///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 25+
//RUNTIME_OPTIONS --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED
//DEPS com.qxotic:jinfer-bom:0.2.0@pom
//DEPS com.qxotic:jinfer-spring-ai com.qxotic:jinfer-models-all
//DEPS org.springframework.ai:spring-ai-client-chat:2.0.1
//DEPS com.qxotic:jam-native com.qxotic:jam-vector
//DEPS org.slf4j:slf4j-nop:2.0.17
import com.qxotic.jinfer.spring.ai.*;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.core.io.UrlResource;
import org.springframework.util.MimeType;
void main() throws Exception {
try (var gemma = JinferChatModel.builder()
.model("unsloth/gemma-4-E2B-it-GGUF:Q4_K_M")
.companion("media", "unsloth/gemma-4-E2B-it-GGUF/mmproj-BF16.gguf")
.build()) {
// jfk.wav: JFK's own voice, inaugural address, January 20, 1961
var speech = new UrlResource("https://qxotic.ai/snippets/jfk.wav");
var transcript = ChatClient.create(gemma).prompt()
.user(u -> u.text("Transcribe this recording.")
.media(MimeType.valueOf("audio/wav"), speech))
.call().content();
System.out.println(transcript);
}
}$ jbang Audio.java # "ask not what your country can do for you ..." · every word heard
jbang header + imports
///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 25+
//RUNTIME_OPTIONS --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED
//DEPS com.qxotic:jinfer-bom:0.2.0@pom
//DEPS com.qxotic:jinfer-spring-ai com.qxotic:jinfer-models-all
//DEPS org.springframework.ai:spring-ai-client-chat:2.0.1
//DEPS com.qxotic:jam-native com.qxotic:jam-vector
//DEPS org.slf4j:slf4j-nop:2.0.17
import com.qxotic.jinfer.spring.ai.*;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.core.io.UrlResource;
import org.springframework.util.MimeTypeUtils;
void main() throws Exception {
try (var vision = JinferChatModel.builder()
.model("LiquidAI/LFM2.5-VL-3B-GGUF:Q8_0")
.companion("media", "LiquidAI/LFM2.5-VL-3B-GGUF/mmproj-LFM2.5-VL-3B-Q8_0.gguf")
.build()) {
// The windmills of Consuegra, La Mancha: Don Quixote's "giants"
var windmills = new UrlResource("https://qxotic.ai/snippets/consuegra.jpg");
var answer = ChatClient.create(vision).prompt()
.user(u -> u.text("Don Quixote saw giants here. What do you see?")
.media(MimeTypeUtils.IMAGE_JPEG, windmills))
.call().content();
System.out.println(answer);
}
}$ jbang Vision.java # "several traditional windmills standing on a hill under a clear blue sky"
jbang header + imports
///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 25+
//RUNTIME_OPTIONS --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED
//DEPS com.qxotic:jinfer-bom:0.2.0@pom
//DEPS com.qxotic:jinfer-spring-ai com.qxotic:jinfer-models-all
//DEPS com.qxotic:jam-native com.qxotic:jam-vector
//DEPS org.slf4j:slf4j-nop:2.0.17
import com.qxotic.jinfer.spring.ai.JinferEmbeddingModel;
void main() {
try (var emb = JinferEmbeddingModel.builder()
.model("LiquidAI/LFM2.5-Embedding-350M-GGUF:Q8_0")
.build()) {
var a = emb.embed("AI on the JVM");
var b = emb.embed("the JVM thinks now");
var c = emb.embed("the python shed its skin");
System.out.printf("similar %.3f%n", cosineSimilarity(a, b));
System.out.printf("unrelated %.3f%n", cosineSimilarity(a, c));
}
}
cosineSimilarity helper
float cosineSimilarity(float[] a, float[] b) {
float dot = 0, na = 0, nb = 0;
for (int i = 0; i < a.length; i++) {
dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i];
}
return (float) (dot / Math.sqrt(na * nb));
}$ jbang Embed.java # similar 0.609 · unrelated 0.081
Benchmarking is hard ... take these numbers as indicative, not definitive, always measure yourself.
jinfer-bench vs. llama-bench on CPU, ↑ higher is better.
Local AI, end-to-end on the JVM: no sidecars, no services, no IPC.
No Python runtime, no ONNX, no glue code. Every component, from tokenizers to the inference engine, built for the JVM, not bolted onto it.
Ships as a single self-contained binary: millisecond startup, small footprint, no JVM at runtime.
One Tensor API, seven backends: Java, C, CUDA, HIP, Metal, OpenCL, and Mojo.