JuL — Juste un LLM
Typed decisions on your machine, with the model of your choice.
No training, no API, no task learned by heart.
Website · Quickstart · Results · Docs · Showcases
JuL answers typed questions about a piece of text: pick one option, say yes or no, give a score on a scale. Every answer comes with a probability, and everything runs on your machine.
It doesn't learn your task. It takes a general-purpose model, reads the vector the model builds for your text, and compares it with the vectors of your option descriptions. Your task never ends up in the weights, so you can change the options and the next call answers the new question. You can also swap the model underneath without touching your code.
JuL is stopped one step before its first syllable and the answer is read straight out of its hidden states: no monologue, no reasoning trace, no opinion on the matter — nobody asked for one. It has nothing to say, and it says it in 55 milliseconds.
And when it is off-key on your data, there is client.autotune(...), or jul autotune from the shell.
Why JuL
- It works zero-shot. The default model scores 0.857 on Jev's public benchmark (300 examples)
without seeing a single example of the three tasks. Two of those tasks are in MTEB, which embedding
models train on, so read the clean one first: AG News, where
wemm-4bgets 0.95. Results - It holds up with many options. Banking77 has 72 intents and the default model gets 0.87 on it (same MTEB caveat). Option vectors are computed once and cached, so adding options barely changes the cost of a call.
- Its probabilities mean something. ECE is 0.084 on the same benchmark: on average, the confidence it reports is within about 8 points of how often it's actually right. That's what lets you automate above a threshold and send the rest to a human.
- It runs locally. Apache 2.0, 2.6 GB, 55 ms per decision on a Mac.
f2llm-1.7bfits in 1 GB and answers in 24 ms. You pay nothing per call and the text stays on your machine. - The model is replaceable. We've measured 17 so far, from a 90 MB encoder to 9B embedding models.
When a better embedding model ships,
jul models addwires it in and your application code stays as it is. - You can tune it when you have labels.
autotune(...)fits a small head on top of the vectors, keeps it only if it beats zero-shot in cross-validation, and leaves the model alone. - It's a drop-in for the Jev SDK: same imports, same calls, same response shapes. Change the import
and you're done.
For callers that aren't Python,
jul servespeaks the Jev HTTP protocol on your machine: serving over HTTP.
Install
pip install jul
jul setup # picks MLX or PyTorch, installs it, downloads the default model, runs one decision
Or choose the backend yourself:
pip install "jul[mlx]" # Apple Silicon
pip install "jul[torch]" # Linux / Windows / CUDA / CPU
pip install "jul[onnx]" # CPU only, no torch — for deploying a packed bundle
Python ≥ 3.10. Weights are downloaded once from the Hugging Face Hub (2.6 GB for the default). Backends, devices, extras and an agent-ready install prompt: docs/installation.md.
Quickstart
# from typesafe_sdk import TypeSafeClient, Choice, Noul, Score
from jul import TypeSafeClient, Choice, Noul, Score
client = TypeSafeClient() # wemm-4b-4bit, or model="minicpm5-2b"
response = client.system_one(
state={"ticket": "I was charged twice for my subscription this month."},
questions={
"team": Choice(instructions="Which team should handle this ticket?",
criteria={"billing": "payments, invoices, refunds",
"technical": "bugs, errors, crashes",
"sales": "pricing, plans, demos"}),
"is_bug": Noul(instructions="Does the message report a software bug?"),
"frustration": Score(instructions="How frustrated is the customer?",
criteria=["Calm", "Frustrated but civil", "Very angry"]),
},
)
response.choices["team"].choice # "billing"
response.choices["team"].probabilities # {"billing": 0.88, "technical": 0.11, "sales": 0.01}
response.nouls["is_bug"].noul # 0.12
response.scores["frustration"].score # 1.15
AsyncTypeSafeClient has the same API, awaitable. Arguments that only make sense for a remote API
(api_key, retry, …) are accepted and ignored.
One thing to know before writing your own questions: the model compares the text with the option
description, not with its key. "billing" is just the name you get back; "payments, invoices, refunds" is what does the work. Write descriptions a colleague would understand.
From the shell:
jul ask choice "Which team should handle this ticket?" \
-o billing:"payments, invoices" -o technical:"bugs, errors" \
--state "I was charged twice"
How it works
Each option description goes through the same prompt template as the text, and the answer is the
option whose hidden-state vector is closest to the text's (cosine similarity, after subtracting a
center). A softmax over cosine / tau turns those similarities into probabilities. The prompt prefix,
the option vectors and the center don't depend on the input, so they're computed once and cached; a
call only pays for its own tokens.
Layers, temperatures and centers were fitted for each model on development sets that the benchmark never touches. Other ways of reading an answer (option-letter logits, trained decision models, tuned heads) are in docs/models.md.
Results
This is Jev's published benchmark: 300 examples over three tasks. Every row was produced through the
public API with scripts/bench_jul.py, zero-shot, without any example of these tasks.
| Model | AG News | Banking77 (72 options) | Emotion | Mean | ECE ↓ | p50 | Memory |
|---|---|---|---|---|---|---|---|
wemm-4b |
0.95 | 0.88 | 0.80 | 0.877 | 0.112 | 78 ms | 4.5 GB |
wemm-9b |
0.97 | 0.84 | 0.78 | 0.863 | 0.114 | 138 ms | 9.0 GB |
wemm-4b-4bit (default) |
0.90 | 0.87 | 0.80 | 0.857 | 0.084 | 55 ms | 2.6 GB |
f2llm-4b |
0.89 | 0.82 | 0.81 | 0.840 | 0.090 | 46 ms | 3.0 GB |
f2llm-1.7b |
0.91 | 0.67 | 0.87 | 0.817 | 0.082 | 24 ms | 1.0 GB |
minicpm5-2b (fast) |
0.80 | 0.59 | 0.46 | 0.617 | 0.113 | 64 ms ¹ | 2.7 GB |
| Jev, published, for reference | 0.91 | 0.87 | 0.48 | 0.753 | 0.156 | 246 ms ² | hosted |
A few things to keep in mind. With 100 rows per task, each cell is ±5 points and the mean ±3.
Banking77 and Emotion are in MTEB, and the embedding models here (wemm-*, f2llm-*) were trained
on MTEB, so they've probably seen those texts before. AG News is the clean comparison: 0.95 for
wemm-4b, 0.91 for Jev. Latencies are p50 on an M5 Max (¹ on an M4 Pro); Jev's (²) includes the
network.
We read seventeen models on these rows. The full table, the tuned results, the decision model and the commands to reproduce all of it are in docs/benchmarks.md.
We haven't tested domains far from this kind of text yet (sensor logs, chemistry and so on), and every setting was fitted on English.
The baseline worth remembering
Before paying for any of this, look at what a bag of words does. Same 300 rows, trained on the same
1000 labeled examples, no LLM at all (scripts/bench_tfidf.py):
| No LLM | AG News | Banking77 | Emotion | Mean | p50 | Training |
|---|---|---|---|---|---|---|
| TF-IDF + linear SVM | 0.88 | 0.76 | 0.43 | 0.690 | 0.17 ms | 0.1 s on CPU |
It lands 6.3 points behind Jev and is about 1400× faster. The only task where it clearly loses is Emotion, because telling feelings apart takes meaning, not vocabulary. If you have labels and your problem looks like sorting by topic or intent, try this first. It takes a minute.
CI re-measures these accuracies on every PR (numbers.yml). The
expected values are also written in that workflow (CLAIMED), so update both together.
Models
| Preset | Size | Jev bench, zero-shot | + autotune, 1000 labels | Notes |
|---|---|---|---|---|
wemm-4b-4bit (default, alias accurate) |
2.6 GB | 0.857 | 0.897 | built in |
minicpm5-2b (alias fast) |
2.7 GB | 0.617 | 0.757 | built in, 62 ms on an M4 Pro |
minicpm5-2b-decision |
1.3 GB | see benchmarks | — | trained decision model, jul models add |
e5-small (ONNX, 8-bit) |
0.09 GB | 0.543 | 0.713 (0.790 hybrid head) | encoder, 6 ms per text on an M4 Pro |
To use another model, run jul models add <name> --repo <hf-repo>. It fits the layer, center and
temperature on the dev sets. The 17 models we measured, encoders, decision models and every setting
are listed in docs/models.md.
Documentation
| Guide | What's inside |
|---|---|
| Installation | backends (MLX, PyTorch, ONNX), devices, batching, jul setup, model downloads |
| Models | every model measured, adding a model, encoders, decision models, readings and settings |
| Adapting to your data | Context, autotune(...), hybrid heads, formulations, jul synth |
| Deployment | jul pack, bundles, the ONNX backend, AWS Lambda numbers |
| Serving over HTTP | jul serve: a local server speaking the Jev HTTP protocol, for non-Python callers |
| Command line | every command and file format (questions, labeled data, I/O) |
| Benchmarks | full results, what is and isn't measured, reproducing every number |
| Development | repository layout, test suites |
| Publishing | versioning and the release pipeline |
Looking for a section of the old README? Everything moved to docs/.
- Install, backends,
jul setup→ installation - The models, every model measured, adding a model, micro models, decision models, how it answers, every reading and setting, the two presets → models
- Use it as a drop-in for Jev → Quickstart
- Context,
autotune(...),features=,formulations=,jul synth→ tuning jul packand the onnx backend → deployment- Command line, file formats → cli
- Results, the decision model, embedding models, what is measured, next steps, reproducing the measurements → benchmarks
- Layout, tests → development
Showcases
jul-showcases is a set of seven small demos
by Jérôme Guyon. Each one takes an idea from
jevable.com and runs it locally with JuL. There's a form that branches on its
own answers, re-ranking by intent, notification triage and prompt-difficulty routing. One demo triages
50,000 real support tickets in 668 s for $0, another uses autotune to take a fast model from 82.0%
to 96.5%, and the last is an on-device browser agent that books a train on SNCF Connect.
Contributing
Bug reports, measurements on your own data and pull requests are all welcome on GitHub.
git clone https://github.com/usejul/jul && cd jul
pip install -e ".[dev]"
pytest tests # fast suite, no model, ~30 s
JUL_SLOW=1 pytest tests # full suite, downloads the presets
One rule for experiments: tune on the dev datasets, and run the benchmark once at the end. The steps are in reproducing the measurements.
License
Apache License 2.0. See LICENSE and NOTICE.
The decision-model format and its pointer readout come from Kev
(Jared Palmer, Apache 2.0). minicpm5-2b-decision is MiniCPM5-2B
(OpenBMB, Apache 2.0), trained with Kev's code on Kev's data plus ours. JuL follows TypeSafe's public
System One API. It uses no TypeSafe or Jev code, weights or outputs.
Release files for jul 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jul-0.2.0.tar.gz | 233.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jul-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 370.7 kB
Release files / jul-0.2.0.tar.gz
| Download URL | jul-0.2.0.tar.gz |
|---|---|
| Size | 233.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
752b35364463f0723e612d20bda12f24365ba19b8c70b87234a9931c2b9fbfe5
|
|
BLAKE2b-256 checksum How to use checksums |
a6f01618ce5e5c6bc5237e41cd738a02f68c00d3c1493d08cf20d247e6f931ae
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / jul-0.2.0-py3-none-any.whl
| Download URL | jul-0.2.0-py3-none-any.whl |
|---|---|
| Size | 136.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
59fda5b25ceca8062e7b5d0d5dbe8ac9110d64039d8a3344f5111bd2301efb42
|
|
BLAKE2b-256 checksum How to use checksums |
a9454a1cdfbf3a591e98a4199df68cde1736e994b7e16188bdb78fb4a85a0b49
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency log