ModelJars is a community-owned marker-JAR convention for local and remote model artifacts. It borrows the useful WebJars idea of using normal JVM dependency coordinates and classpath metadata. Large model weights remain external; compact, license-compatible model payloads can be bundled when doing so makes the artifact directly usable.
Every marker JAR carries machine-readable runtime and catalog metadata:
META-INF/modeljars/registry.properties
META-INF/modeljars/model.json
META-INF/modeljars/performance-v1.properties
META-INF/modeljars/performance-v1.json
META-INF/modeljars/qualifications-v1.properties
META-INF/modeljars/qualifications-v1.json
Descriptors point to upstream model locations or bundled resources, checksums, licenses, formats,
quantization variants, runtime feature flags, and backend compatibility. A bundled payload lives
below META-INF/modeljars/models/<catalog-id>/ and is verified against the same size and SHA-256
metadata as an external model.
Model identity has one source of truth, catalog/models.json; controlled performance evidence has
the independent versioned source catalog/performance-profiles.json. Gradle generates the aggregate
classpath candidate catalog and one marker build per entry. The aggregate JAR embeds
META-INF/modeljars/catalog.json, so adding a candidate does not require a new Gradle module or
source folder.
The public site and publication plan use catalog/qualifications.json as a separate release
boundary. They include only the qualified subset whose artifact SHA-256 and byte size exactly match
the candidate catalog. Recording candidate metadata does not create a public model page or authorize
publication.
The modeljars CLI is a self-contained GraalVM native executable; using it does not require a JDK.
It follows the familiar local-model workflow used by tools such as Ollama:
# macOS or Linux
brew install integrallis/tap/modeljars
# macOS or Linux without Homebrew
curl -fsSL https://raw.githubusercontent.com/ModelJars/modeljars/main/install.sh | sh
# Windows
scoop bucket add integrallis https://github.com/integrallis/scoop-bucket
scoop install modeljarsSearch or filter the qualified catalog, inspect immutable provenance, prefetch weights, see what is cached, and check what the current machine can actually run:
# Start an interactive session
modeljars
# Or execute any command once
modeljars search gemma
modeljars search --capability embedding --sort size
modeljars search fintech
modeljars search gemma --details
modeljars show ggml_org_gemma_4_26b_a4b_it_gguf_q4_k_m
modeljars pull ggml_org_gemma_4_26b_a4b_it_gguf_q4_k_m
modeljars ls
modeljars info
modeljars snippet ggml_org_gemma_4_26b_a4b_it_gguf_q4_k_m --tool mavenpull uses the same content-addressed cache as the JVM Runtime. It downloads from the descriptor's
exact immutable URL, verifies byte size and SHA-256, and never starts inference. Release automation
builds native binaries for macOS, Linux, and Windows on their host architectures and publishes an
executable fallback JAR to Maven Central and GitHub Packages. SDKMAN multi-platform archives are
also generated; publication begins once the modeljars candidate completes SDKMAN vendor
onboarding.
Interactive output uses responsive, aligned tables and color only when stdout is a terminal.
Catalog and cache listings are compact by default; add --details to show complete capability and
backend continuation fields without truncation. list --coordinates independently adds the exact
marker coordinate. Search includes catalog domains/tags and common discovery aliases, such as
fintech for finance.
Every discovery command also supports --output json for automation and --output plain for stable
line-oriented output; NO_COLOR and --color never disable ANSI output. modeljars info reports
CPU, physical and logical cores, SIMD, memory, graphics hardware, Java/native runtime, backend
eligibility, catalog capability counts, and local cache usage. A detected GPU is reported separately
from a usable inference backend so hardware inventory is never mistaken for supported acceleration.
Run modeljars help or modeljars <command> --help for the complete command surface. Use
modeljars snippet <model> --tool maven or --tool gradle-kotlin for a copy-ready dependency;
coordinates, coords, dependency, and deps are aliases for the same command. With no
arguments, modeljars opens a prompt with history and tab completion; exit, quit, or Ctrl-D
returns to the shell. Supplying any command keeps the normal one-shot behavior used by scripts.
Applications use the stable JVM Runtime artifact and add one model dependency for every model they intend to ship:
dependencies {
implementation("org.modeljars:modeljars:0.1.8")
implementation(
"org.modeljars.huggingface:" +
"ggml-org.qwen3-0.6b-gguf.q4_0:" +
"3.0.0-q4_0.1",
)
}<dependency>
<groupId>org.modeljars</groupId>
<artifactId>modeljars</artifactId>
<version>0.1.8</version>
</dependency>
<dependency>
<groupId>org.modeljars.huggingface</groupId>
<artifactId>ggml-org.qwen3-0.6b-gguf.q4_0</artifactId>
<version>3.0.0-q4_0.1</version>
</dependency>modeljars exposes modeljars-core, Models 0.3.2, and both Models execution backends. Each marker
JAR contributes its own descriptor, qualification evidence, performance profiles, and generated
Java reference. Applications using the JVM Runtime require Java 25 or newer. modeljars-core and
the fallback CLI JAR remain usable by Java 21 registry and build tooling without the Models runtime.
Marker dependencies are build-time model-version declarations and contain no transitive runtime dependencies. Add each selected marker in compile scope so its generated reference is available to application source.
The production-qualified subset is searchable at modeljars.org. A compact Qwen marker is:
org.modeljars.huggingface:ggml-org.qwen3-0.6b-gguf.q4_0:3.0.0-q4_0.1
It resolves the upstream source:
hf://ggml-org/Qwen3-0.6B-GGUF
For this launch catalog, external weights are downloaded directly from an exact, commit-pinned
Hugging Face revision. The marker JAR contains metadata rather than the large weights, including the
source page, immutable download URL, revision, byte count, and SHA-256. The JVM Runtime and CLI store
verified weights in a content-addressed cache below ${user.home}/.modeljars/cache/sha256/.
Application code never constructs or passes that path. Run modeljars show <model> to inspect all
of that provenance before downloading.
Qwen2.5-Coder markers include:
org.modeljars.huggingface:qwen.qwen2.5-coder-0.5b-instruct-gguf.q4_0:2.5.0-q4_0.1
org.modeljars.huggingface:qwen.qwen2.5-coder-0.5b-instruct-gguf.q8_0:2.5.0-q8_0.1
org.modeljars.huggingface:qwen.qwen2.5-coder-1.5b-instruct-gguf.q4_0:2.5.0-q4_0.1
org.modeljars.huggingface:qwen.qwen2.5-coder-1.5b-instruct-gguf.q8_0:2.5.0-q8_0.1
The catalog also includes the bundled 40,000-term WordTour semantic-order artifact:
org.modeljars.github:joisino.wordtour-glove-6b-300d.optimal:1.0.0-optimal.1
import static org.modeljars.catalog.Qwen3_0_6b_Q4_0.MODEL;
import com.integrallis.models.api.InferenceContextWindow;
import com.integrallis.models.api.ModelPrompt;
import com.integrallis.models.api.Tokenizer;
import com.integrallis.models.runtime.InferencePipeline;
var options = SamplingOptions.builder()
.temperature(0).maxTokens(128).build();
try (var runtime = ModelJars.openRuntime(MODEL)) {
InferencePipeline pipeline = runtime.pipeline();
Tokenizer tokenizer = runtime.tokenizer();
InferenceContextWindow context = runtime.contextWindow();
ModelPrompt prompt = runtime.chatTemplate().render(
List.of(ChatMessage.user("Name one JVM language.")));
String answer = pipeline.generate(prompt, options);
}ChatTemplate.render(...) returns com.integrallis.models.api.ModelPrompt. ModelPrompt preserves
the distinction between template control tokens and user text; it is intentionally not a String.
runtime.pipeline() exposes the same owning model through the complete Models inference API,
including structured tokenization, metadata, active context capacity and position, prefill,
forward-pass logits, reset, checkpoint, and rewind. runtime.model() remains the high-level
TextGenerationModel view and delegates structured prompts to that pipeline.
ModelJars.openRuntime resolves the exact qualified descriptor, selects its qualified Models backend
and chat template,
downloads missing weights, verifies their size and SHA-256 digest, and applies every non-conflicting
artifact-bound performance profile that matches the current runtime. Profiles with Java launch
requirements apply only when every required JVM argument is active; omitted profiles and missing
arguments remain visible in backend diagnostics. The runtime owns the backend and closes it at the
end of the try block. The older ModelJars.open model-only API remains available when the caller
already owns prompt selection.
Qualified embedding markers use the same path-free loading contract. Pooling, normalization, vector width, artifact digest, and backend come from the marker's equivalence evidence rather than application configuration:
import static org.modeljars.catalog.Ggml_Org_Embeddinggemma_300m_Gguf_Q8_0.MODEL;
try (var embeddings = ModelJars.openEmbedding(MODEL)) {
float[] vector = embeddings.embed("Where is the maintenance schedule?");
}Use ModelJars.openEmbeddingRuntime(MODEL) when the application also needs the exact descriptor
and ModelEmbeddingQualificationRegistry.Entry selected for the loaded model. Applications never
construct a cache path or choose pooling themselves.
Local inference must start Java 25 or newer with the Vector module resolved:
--add-modules=jdk.incubator.vector
ModelJars checks this before resolving or downloading model weights. When automatic selection picks
the Rust/FFM backend, it also checks for --enable-native-access=ALL-UNNAMED before downloading and
explains how to select a qualified Java backend when available.
If an error says class-file version 69.0 but the runtime only recognizes up to 61.0, the model
library was compiled for Java 25 (69) but the process actually launched with Java 17 (61). Java
26 can run Java 25 bytecode; the usual cause is Maven, Gradle, or an IDE using a different JDK than
the shell. Check every launcher involved:
java -version
mvn -v
./gradlew --versionFor Maven's in-process exec:java, pass the module to the Maven JVM:
MAVEN_OPTS="--add-modules=jdk.incubator.vector" mvn exec:javaFor a packaged application, place the option before -jar:
java --add-modules=jdk.incubator.vector -jar application.jarModelJars does not force a LangChain4j or Spring AI version on applications. Add the Models adapter, the framework-neutral grounding module, and the chosen framework explicitly. For LangChain4j:
implementation("com.integrallis:models-rag:0.3.2")
implementation("com.integrallis:models-langchain4j:0.3.2")
implementation("dev.langchain4j:langchain4j:1.17.2")For Spring AI:
implementation("com.integrallis:models-rag:0.3.2")
implementation("com.integrallis:models-spring-ai:0.3.2")
implementation("org.springframework.ai:spring-ai-client-chat:2.0.0")
implementation("org.springframework.ai:spring-ai-rag:2.0.0")Use GroundedRagPrompt.prepare(...) to screen retrieved evidence and construct the canonical prompt.
Place its instructions() in the framework system message and its request() in the user message,
then render both with runtime.chatTemplate().
Offline loading and explicit backend selection are available without exposing the cache path:
var options = ModelLoadOptions.builder()
.offline(true)
.backend(ModelBackend.JAVA)
.build();
try (var model = ModelJars.open(MODEL, options)) {
// The verified artifact must already be in the ModelJars cache.
}Configuration-driven applications can select the same classpath marker by its complete coordinate:
try (var model = ModelJars.open(
"org.modeljars.huggingface:ggml-org.qwen3-0.6b-gguf.q4_0:3.0.0-q4_0.1")) {
// Generate text through the qualified backend.
}Registry APIs remain available when an application needs model metadata without loading inference:
ModelJarDescriptor descriptor =
ModelJarRegistry.fromClasspath().resolve(MODEL).orElseThrow();
Set<String> requiredFeatures = descriptor.features();
ModelDimensions dimensions = descriptor.dimensions();
Optional<ModelMemoryEstimate> baseline =
descriptor.estimateMemory(4096, KvCachePrecision.FLOAT16);Descriptors also expose display name, description, domains, upstream and download links, license link, exact artifact byte size, parameter count, context length, embedding width, total and attention block counts, attention/KV heads, feed-forward width, and MoE dimensions when present. Memory estimates are deliberately limited to model-file bytes plus the requested KV cache. Backend workspace, tensor repacking, allocator overhead, the JVM, and the operating system are excluded.
Feature flags expose requirements and handling metadata such as q4-k, chatml,
community-conversion, and medical-use-warning. Markers created before the feature property was
introduced remain loadable and return an empty set.
Versioned performance profiles are discovered separately and bind every recommendation to the exact marker coordinate, model SHA-256, backend, runtime selector, and reproducible before/after evidence:
ModelPerformanceProfileRegistry profiles =
ModelPerformanceProfileRegistry.fromClasspath();
List<ModelPerformanceProfile> measured = profiles.profilesFor(descriptor);The ModelJars JVM Runtime calls matching(descriptor, backend, runtimeFacts) with the complete
structured runtime fingerprint before loading the selected Models backend. It combines independent
recommendations with different keys, rejects conflicting overlapping profiles at registry load,
and verifies typed Java launch requirements against the active JVM input arguments.
safeForAutomaticSelection() means the profile has recommendations and exact output hashes
matched in its comparison. It does not authorize arbitrary runtime properties or native code;
backends must whitelist supported recommendation keys and retain their own correctness checks. See
Performance profiles for the schema and contribution rules.
The lower-level installer remains available to registry tooling that needs the verified file:
ModelJarRegistry registry = ModelJarRegistry.fromClasspath();
Path artifact = new ModelJarInstaller(registry).install(MODEL);ModelJarInstaller verifies both the byte size and SHA-256 digest before atomically moving the
download into place. Most applications should use ModelJars.open.
Compact bundled payloads use the same verification contract without an installation step:
ModelJarDescriptor descriptor =
registry.resolve(
ModelJar.of("github://joisino/wordtour")
.variant("optimal")
.backend("semantic-order"))
.orElseThrow();
byte[] payload = new ModelJarResourceLoader(
Thread.currentThread().getContextClassLoader()
).readVerified(descriptor);A public ModelJar represents one exact model artifact, not a claim about every conversion or quantization of the upstream model. Qualification pins the artifact and Models revision, runs parser/tokenizer/generation tests, then executes the complete controlled RAG workload using the selected Models backend and library-default properties. Every default generation must succeed with perfect deterministic correctness before any tuning is applied. A separate performance phase checks absolute quality and latency plus same-host performance against Ollama. llama.cpp is retained as a second independent comparator; neither comparator is a runtime dependency.
Embedding artifacts use a separate policy: we test that the model produces the same vectors as
llama.cpp, for a pinned probe set over the same bytes. Agreement is gated at 0.999 cosine, where a
correct run measures 0.99950 and wrong pooling measures 0.66156. Cosine is scale-invariant, so
vector length is gated separately at 1e-3. Evidence is recorded as ModelEmbeddingQualification,
whose tier is SEMANTIC_SEARCH or UNQUALIFIED.
The qualification and submission guide lists host prerequisites, the harness command, acceptance thresholds, evidence files, and pull request steps. “Not yet qualified” means the controlled run has not occurred; it is not a failed result.
./gradlew test verifyCatalog verifyRemoteCatalogMetadata
./gradlew generateSite
npm ci
npm test
npm run catalog:enrichThe generated GitHub Pages site is written to build/site. Individual marker JARs are written
under modeljars-catalog/build/libs/markers. Classpath payloads are fetched from their pinned
source revision during the build and must pass size, digest, format, vocabulary, and uniqueness
checks.
npm run catalog:enrich -- --write uses Hugging Face's official range-aware GGUF parser to update
dimensions from each exact revision-pinned artifact without downloading its tensors. The same
command without --write is the CI verification mode.
The WebJars repositories used as design references are cloned under ../../references:
webjars/webjarswebjars/webjars-locator-corewebjars/webjars-locator-lite
The first implementation follows the locator-lite approach: no startup classpath scan, just well-known metadata resources. A richer scanner and public catalog service can come later.