Typed decisions on your machine, with the model of your choice.
No training, no API, no task learned by heart.
Website · Quickstart · Showcases · Results · Docs
JuL is a Python library that answers typed questions about a piece of text: pick an option
(Choice), say yes or no (Noul), give a score (Score). Each answer comes with a probability. It
has the same API as the SDK of Jev, TypeSafe's hosted decision model.
The model 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. It has nothing to say, and it says it in 55 milliseconds.
It runs on your own machines, from a Mac to a Linux server, so you can keep it running 24/7 on your own infrastructure. Your text never goes to a third party and there is no per-call bill. The model weights are downloaded once from the Hugging Face Hub, or loaded from a local path; after that it runs with no network at all.
wemm-4b 0.95 and Jev 0.91.jul-decision-wemm-4b, the default model on PyTorch, answers 0.849 of
2,108 Choice, Noul and Score questions over 12 task families, Jev 0.873, at 97 ms per decision on one A10G
GPU (per family). On a bench of 300 typed questions written from scratch
(sentiment, finance, support, agent routing, moderation) it answers 0.677, and jul-decision-minicpm5-2b
(alias fast), half its size, 0.680 at 85 ms.f2llm-1.7b (converted
locally, not published yet) fits in 1 GB and answers in 24 ms.jul models add wires in a new one without touching your code,
including other open decision models (Kev and Strands Decider checkpoints, the letter-readout models JevK5,
plumb-4b, Quyet and spark-s1, each read as its own runtime reads it) and Laya on its own runtime.jul bench test.jsonl --models fast,accurate,typesafe answers them with every
model you name, local or remote (Jev, Ollama, Clef, any System One URL), and says which one to pick.jul serve --escalate-to typesafe (or jul.Escalation) answers locally
and sends only the questions below a confidence bar to a bigger decider.usage.truncated_tokens, and
on_long="error" refuses a text longer than the model reads
(input limits).autotune(...) takes the default model from 0.857 to 0.897 with 1000
examples, and keeps the head only if it beats zero-shot.jul serve
speaks the Jev HTTP protocol for other languages.# from typesafe_sdk import TypeSafeClient, Choice, Noul, Score
from jul import TypeSafeClient, Choice, Noul, Score
client = TypeSafeClient() # jul-decision-wemm-4b, 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
The model compares your text with each option's description, so write descriptions a colleague
would understand; the key is only the name you get back. noul is the probability of yes, and score
is the expected level on your scale, from 0 ("Calm") to 2 ("Very angry"). AsyncTypeSafeClient has the same API.
Arguments that only make sense for a remote API (api_key, retry, …) are accepted and ignored, so
code written for Jev runs unchanged.
From the shell, use jul ask. Non-Python callers can use jul serve, which speaks the Jev HTTP
protocol (docs/serve.md).
Eight demos from jul-showcases, each an idea from jevable.com running entirely on device, for $0.
![]() Browser agent on SNCF Connect. JuL picks each action from the page's accessibility tree and the Apple Foundation Model types the city names (macOS 26). Six steps from the homepage to priced results, about 130 ms per decision. |
Ticket triage at scale. 50,000 real support tickets routed in 668 s, 82.9% accurate, for $0, with the small qwen3-embedding-0.6b on an Apple Silicon Mac. At
that rate a million take about 3.7 hours.
|
pip install jul
jul setup # picks MLX or PyTorch, installs it, downloads the default model, runs one decision
Python ≥ 3.10. The default model weighs 2.6 GB and is downloaded once from the Hugging Face Hub.
To pick the backend yourself (jul[mlx], jul[torch], jul[onnx]), see
docs/installation.md.
JuL reads the hidden state the model built for your text and compares it with the vectors of your
option descriptions. Your task never reaches the weights, so you can change the options between two calls, or swap the model, without retraining
anything. When it is off on your data, client.autotune(...) fits a small head on labeled examples
and keeps it only if it beats zero-shot in cross-validation
(docs/tuning.md).
Jev's published benchmark, 300 examples, zero-shot for every JuL model, run with
scripts/bench_jul.py. Each mean is ±3 points. Banking77 and Emotion are in MTEB, which the
wemm-* and f2llm-* models trained on, so AG News is the clean comparison: 0.95 for wemm-4b,
0.91 for Jev, and 0.90 for the default model. The default model's ECE is 0.084 against Jev's 0.156, which is what lets you automate
above a confidence threshold. Every setting was fitted on English, and we haven't tested domains far
from this kind of text (sensor logs, chemistry and so on). Per-task scores, the 18 models measured and
the tuned results are in
docs/benchmarks.md.
The default model, jul-decision-wemm-4b, is WeMM-Embedding-4B with LoRA adapters that read the question and
the text together (usejul/jul-decision-wemm-4b). The
decision bench asks it 2,108 typed questions over 12 task families, in English and French, with texts
written for the bench and never seen in training. PyTorch bf16 on one A10G GPU, one question per call;
Jev through its API, network included in its latency.
| Family | n | Jev | jul-decision-wemm-4b |
|---|---|---|---|
| emotion | 60 | 0.933 | 0.817 |
| finance | 180 | 0.756 | 0.850 |
| hard negatives | 226 | 0.823 | 0.836 |
| intent | 234 | 0.842 | 0.868 |
| legal | 158 | 0.911 | 0.873 |
| moderation | 179 | 0.877 | 0.804 |
| inference (NLI) | 208 | 0.817 | 0.731 |
| relational | 101 | 0.941 | 0.921 |
| routing | 118 | 0.898 | 0.831 |
| sentiment | 120 | 0.983 | 0.925 |
| support | 294 | 0.898 | 0.864 |
| topic | 230 | 0.917 | 0.891 |
| Choice | 917 | 0.924 | 0.883 |
| Noul (yes/no) | 895 | 0.878 | 0.872 |
| Score | 296 | 0.699 | 0.672 |
| English | 1,254 | 0.871 | 0.862 |
| French | 854 | 0.876 | 0.829 |
| All | 2,108 | 0.873 | 0.849 |
| p50 latency | 655 ms | 97 ms |
A family has 60 to 300 questions, so one row moves by ±4 to ±12 points: read the types and the total first. The bench shares its task families and label sets with the training data (not its texts); on the four label sets of jul's dev, never trained on, the adapters take Choice from 0.707 to 0.730. Emotion is where Jev is furthest ahead, and inference is where the model is weakest. On a Mac (MLX, 4-bit) the adapters are not attached yet: the default model reads every question with the vectors, and the Jev benchmark numbers above are its numbers.
A TF-IDF + linear SVM, trained on 1000 labeled examples with no LLM at all, scores 0.88 on AG News,
0.76 on Banking77 and 0.43 on Emotion: a mean of 0.690 at 0.17 ms per prediction
(scripts/bench_tfidf.py). It is 11 points behind the default model on Banking77 and far behind on
Emotion, where telling feelings apart takes meaning, not vocabulary. If you have labels and your problem is
sorting by topic or intent, try it first. CI re-measures these numbers on every PR
(numbers.yml, CLAIMED),
so update both together.
| Preset | Size | Jev bench, zero-shot | + autotune, 1000 labels | Notes |
|---|---|---|---|---|
jul-decision-wemm-4b (default, alias accurate) | 10.3 GB bf16 + 0.07 GB adapters (PyTorch); 2.6 GB (MLX) | 0.857 ² | 0.897 ² | built in; WeMM-Embedding-4B with LoRA adapters that read Noul and Choice with the question and the text together (Score on the vectors): 0.849 on the decision bench, Jev 0.873; attached on PyTorch, MLX reads it as wemm-4b-4bit for now |
wemm-4b-4bit | 2.6 GB | 0.857 | 0.897 | built in, vectors only |
minicpm5-2b | 2.7 GB | 0.617 | 0.757 | built in, 64 ms on an M4 Pro |
jul-decision-minicpm5-2b (alias fast) | 5.0 GB bf16 (PyTorch); 1.4 GB (MLX) | — | — | built in; MiniCPM5-2B trained to answer typed questions: a pointer head for Choice and Noul, Score on its own vectors; 0.680 on our hand-written bench (the default: 0.677) at 85 ms; no autotune (decision models) |
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; needs an ONNX export first, see models |
jul-decision-wemm-4b-4bit (MLX, 4-bit) | 2.6 GB (+0.07 GB adapters) | 0.857 ¹ | 0.897 ¹ | the default model with a LoRA cross model on the same weights for Noul and Score (yes/no 0.841 on Kev's typed decisions, against 0.762 with vectors; ~115 ms per yes/no on an M4 Pro); one model in memory, jul models add |
jul-decision-e5-small (ONNX, 8-bit) | 0.09 GB (+0.09 GB cross model) | 0.557 | 0.723 (0.780 hybrid head) | e5-small trained on jul decisions, with a cross model for Noul and Score (yes/no 0.726 on Kev's typed decisions, against 0.579 with vectors); runs in AWS Lambda, jul models add |
¹ Choice questions, the only ones in the Jev benchmark, are read by the vectors of wemm-4b-4bit, which the
adapters leave untouched (switched off, the features are the same): its numbers.
² Measured on MLX, where the default model reads with the vectors of wemm-4b-4bit. On PyTorch its Choice
reading mixes in the adapters; the Jev benchmark has not been rerun there yet.
To use another model, run jul models add <name> --repo <hf-repo>. It fits the layer, center and
temperature on the dev sets; a decision model (a decision.json, a Strands Decider checkpoint, a
letter-readout model) brings its own format and is read as is. The 18 models we measured, encoders, decision
models, Laya, embeddings APIs and every setting are listed in
docs/models.md; how to pick among them, in
docs/hub.md.
jul-decision-e5-small runs inside an AWS Lambda function, model included in the package: 17 ms per
message and $0.59 per million at 1,769 MB, a 2.4 s cold start, no GPU and no server. Jev is a hosted
API and CLM-8B needs a GPU server; to our knowledge no other typed-decision library puts its model in a
Lambda. It is not a general-purpose endpoint: it is a small model embedded in a function that decides
one precise thing on every event, with the questions fixed at build time, and it should be tuned on
labeled examples of that thing (autotune: 0.557 zero-shot, 0.780 tuned on the Jev bench). jul pack
ships only the model(s) the questions need. Step by step, with the AWS CLI or CDK:
docs/aws-lambda.md.
jul setupjul models addContext, autotune(...), hybrid heads, jul synthjul pack, ONNX bundles, AWS Lambdajul serve, escalation to Jev, Clef or any System One serverjul bench on your own dataIssues, measurements on your own data and pull requests are welcome on
GitHub. pip install -e ".[dev]", then pytest tests (no
model, about 30 s). Tune on the dev datasets and run the benchmark once at the end
(how).
Apache 2.0, see LICENSE and
NOTICE. The decision-model format comes from
Kev (Jared Palmer, Apache 2.0) and jul-decision-minicpm5-2b is
MiniCPM5-2B (OpenBMB, Apache 2.0) trained with Kev's
code. JuL follows TypeSafe's public System One API and uses no TypeSafe or Jev code, weights or
outputs.
Typed decisions on your machine, with the model of your choice.
No training, no API, no task learned by heart.
Website · Quickstart · Showcases · Results · Docs
JuL is a Python library that answers typed questions about a piece of text: pick an option
(Choice), say yes or no (Noul), give a score (Score). Each answer comes with a probability. It
has the same API as the SDK of Jev, TypeSafe's hosted decision model.
The model 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. It has nothing to say, and it says it in 55 milliseconds.
It runs on your own machines, from a Mac to a Linux server, so you can keep it running 24/7 on your own infrastructure. Your text never goes to a third party and there is no per-call bill. The model weights are downloaded once from the Hugging Face Hub, or loaded from a local path; after that it runs with no network at all.
wemm-4b 0.95 and Jev 0.91.jul-decision-wemm-4b, the default model on PyTorch, answers 0.849 of
2,108 Choice, Noul and Score questions over 12 task families, Jev 0.873, at 97 ms per decision on one A10G
GPU (per family). On a bench of 300 typed questions written from scratch
(sentiment, finance, support, agent routing, moderation) it answers 0.677, and jul-decision-minicpm5-2b
(alias fast), half its size, 0.680 at 85 ms.f2llm-1.7b (converted
locally, not published yet) fits in 1 GB and answers in 24 ms.jul models add wires in a new one without touching your code,
including other open decision models (Kev and Strands Decider checkpoints, the letter-readout models JevK5,
plumb-4b, Quyet and spark-s1, each read as its own runtime reads it) and Laya on its own runtime.jul bench test.jsonl --models fast,accurate,typesafe answers them with every
model you name, local or remote (Jev, Ollama, Clef, any System One URL), and says which one to pick.jul serve --escalate-to typesafe (or jul.Escalation) answers locally
and sends only the questions below a confidence bar to a bigger decider.usage.truncated_tokens, and
on_long="error" refuses a text longer than the model reads
(input limits).autotune(...) takes the default model from 0.857 to 0.897 with 1000
examples, and keeps the head only if it beats zero-shot.jul serve
speaks the Jev HTTP protocol for other languages.# from typesafe_sdk import TypeSafeClient, Choice, Noul, Score
from jul import TypeSafeClient, Choice, Noul, Score
client = TypeSafeClient() # jul-decision-wemm-4b, 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
The model compares your text with each option's description, so write descriptions a colleague
would understand; the key is only the name you get back. noul is the probability of yes, and score
is the expected level on your scale, from 0 ("Calm") to 2 ("Very angry"). AsyncTypeSafeClient has the same API.
Arguments that only make sense for a remote API (api_key, retry, …) are accepted and ignored, so
code written for Jev runs unchanged.
From the shell, use jul ask. Non-Python callers can use jul serve, which speaks the Jev HTTP
protocol (docs/serve.md).
Eight demos from jul-showcases, each an idea from jevable.com running entirely on device, for $0.
![]() Browser agent on SNCF Connect. JuL picks each action from the page's accessibility tree and the Apple Foundation Model types the city names (macOS 26). Six steps from the homepage to priced results, about 130 ms per decision. |
Ticket triage at scale. 50,000 real support tickets routed in 668 s, 82.9% accurate, for $0, with the small qwen3-embedding-0.6b on an Apple Silicon Mac. At
that rate a million take about 3.7 hours.
|
pip install jul
jul setup # picks MLX or PyTorch, installs it, downloads the default model, runs one decision
Python ≥ 3.10. The default model weighs 2.6 GB and is downloaded once from the Hugging Face Hub.
To pick the backend yourself (jul[mlx], jul[torch], jul[onnx]), see
docs/installation.md.
JuL reads the hidden state the model built for your text and compares it with the vectors of your
option descriptions. Your task never reaches the weights, so you can change the options between two calls, or swap the model, without retraining
anything. When it is off on your data, client.autotune(...) fits a small head on labeled examples
and keeps it only if it beats zero-shot in cross-validation
(docs/tuning.md).
Jev's published benchmark, 300 examples, zero-shot for every JuL model, run with
scripts/bench_jul.py. Each mean is ±3 points. Banking77 and Emotion are in MTEB, which the
wemm-* and f2llm-* models trained on, so AG News is the clean comparison: 0.95 for wemm-4b,
0.91 for Jev, and 0.90 for the default model. The default model's ECE is 0.084 against Jev's 0.156, which is what lets you automate
above a confidence threshold. Every setting was fitted on English, and we haven't tested domains far
from this kind of text (sensor logs, chemistry and so on). Per-task scores, the 18 models measured and
the tuned results are in
docs/benchmarks.md.
The default model, jul-decision-wemm-4b, is WeMM-Embedding-4B with LoRA adapters that read the question and
the text together (usejul/jul-decision-wemm-4b). The
decision bench asks it 2,108 typed questions over 12 task families, in English and French, with texts
written for the bench and never seen in training. PyTorch bf16 on one A10G GPU, one question per call;
Jev through its API, network included in its latency.
| Family | n | Jev | jul-decision-wemm-4b |
|---|---|---|---|
| emotion | 60 | 0.933 | 0.817 |
| finance | 180 | 0.756 | 0.850 |
| hard negatives | 226 | 0.823 | 0.836 |
| intent | 234 | 0.842 | 0.868 |
| legal | 158 | 0.911 | 0.873 |
| moderation | 179 | 0.877 | 0.804 |
| inference (NLI) | 208 | 0.817 | 0.731 |
| relational | 101 | 0.941 | 0.921 |
| routing | 118 | 0.898 | 0.831 |
| sentiment | 120 | 0.983 | 0.925 |
| support | 294 | 0.898 | 0.864 |
| topic | 230 | 0.917 | 0.891 |
| Choice | 917 | 0.924 | 0.883 |
| Noul (yes/no) | 895 | 0.878 | 0.872 |
| Score | 296 | 0.699 | 0.672 |
| English | 1,254 | 0.871 | 0.862 |
| French | 854 | 0.876 | 0.829 |
| All | 2,108 | 0.873 | 0.849 |
| p50 latency | 655 ms | 97 ms |
A family has 60 to 300 questions, so one row moves by ±4 to ±12 points: read the types and the total first. The bench shares its task families and label sets with the training data (not its texts); on the four label sets of jul's dev, never trained on, the adapters take Choice from 0.707 to 0.730. Emotion is where Jev is furthest ahead, and inference is where the model is weakest. On a Mac (MLX, 4-bit) the adapters are not attached yet: the default model reads every question with the vectors, and the Jev benchmark numbers above are its numbers.
A TF-IDF + linear SVM, trained on 1000 labeled examples with no LLM at all, scores 0.88 on AG News,
0.76 on Banking77 and 0.43 on Emotion: a mean of 0.690 at 0.17 ms per prediction
(scripts/bench_tfidf.py). It is 11 points behind the default model on Banking77 and far behind on
Emotion, where telling feelings apart takes meaning, not vocabulary. If you have labels and your problem is
sorting by topic or intent, try it first. CI re-measures these numbers on every PR
(numbers.yml, CLAIMED),
so update both together.
| Preset | Size | Jev bench, zero-shot | + autotune, 1000 labels | Notes |
|---|---|---|---|---|
jul-decision-wemm-4b (default, alias accurate) | 10.3 GB bf16 + 0.07 GB adapters (PyTorch); 2.6 GB (MLX) | 0.857 ² | 0.897 ² | built in; WeMM-Embedding-4B with LoRA adapters that read Noul and Choice with the question and the text together (Score on the vectors): 0.849 on the decision bench, Jev 0.873; attached on PyTorch, MLX reads it as wemm-4b-4bit for now |
wemm-4b-4bit | 2.6 GB | 0.857 | 0.897 | built in, vectors only |
minicpm5-2b | 2.7 GB | 0.617 | 0.757 | built in, 64 ms on an M4 Pro |
jul-decision-minicpm5-2b (alias fast) | 5.0 GB bf16 (PyTorch); 1.4 GB (MLX) | — | — | built in; MiniCPM5-2B trained to answer typed questions: a pointer head for Choice and Noul, Score on its own vectors; 0.680 on our hand-written bench (the default: 0.677) at 85 ms; no autotune (decision models) |
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; needs an ONNX export first, see models |
jul-decision-wemm-4b-4bit (MLX, 4-bit) | 2.6 GB (+0.07 GB adapters) | 0.857 ¹ | 0.897 ¹ | the default model with a LoRA cross model on the same weights for Noul and Score (yes/no 0.841 on Kev's typed decisions, against 0.762 with vectors; ~115 ms per yes/no on an M4 Pro); one model in memory, jul models add |
jul-decision-e5-small (ONNX, 8-bit) | 0.09 GB (+0.09 GB cross model) | 0.557 | 0.723 (0.780 hybrid head) | e5-small trained on jul decisions, with a cross model for Noul and Score (yes/no 0.726 on Kev's typed decisions, against 0.579 with vectors); runs in AWS Lambda, jul models add |
¹ Choice questions, the only ones in the Jev benchmark, are read by the vectors of wemm-4b-4bit, which the
adapters leave untouched (switched off, the features are the same): its numbers.
² Measured on MLX, where the default model reads with the vectors of wemm-4b-4bit. On PyTorch its Choice
reading mixes in the adapters; the Jev benchmark has not been rerun there yet.
To use another model, run jul models add <name> --repo <hf-repo>. It fits the layer, center and
temperature on the dev sets; a decision model (a decision.json, a Strands Decider checkpoint, a
letter-readout model) brings its own format and is read as is. The 18 models we measured, encoders, decision
models, Laya, embeddings APIs and every setting are listed in
docs/models.md; how to pick among them, in
docs/hub.md.
jul-decision-e5-small runs inside an AWS Lambda function, model included in the package: 17 ms per
message and $0.59 per million at 1,769 MB, a 2.4 s cold start, no GPU and no server. Jev is a hosted
API and CLM-8B needs a GPU server; to our knowledge no other typed-decision library puts its model in a
Lambda. It is not a general-purpose endpoint: it is a small model embedded in a function that decides
one precise thing on every event, with the questions fixed at build time, and it should be tuned on
labeled examples of that thing (autotune: 0.557 zero-shot, 0.780 tuned on the Jev bench). jul pack
ships only the model(s) the questions need. Step by step, with the AWS CLI or CDK:
docs/aws-lambda.md.
jul setupjul models addContext, autotune(...), hybrid heads, jul synthjul pack, ONNX bundles, AWS Lambdajul serve, escalation to Jev, Clef or any System One serverjul bench on your own dataIssues, measurements on your own data and pull requests are welcome on
GitHub. pip install -e ".[dev]", then pytest tests (no
model, about 30 s). Tune on the dev datasets and run the benchmark once at the end
(how).
Apache 2.0, see LICENSE and
NOTICE. The decision-model format comes from
Kev (Jared Palmer, Apache 2.0) and jul-decision-minicpm5-2b is
MiniCPM5-2B (OpenBMB, Apache 2.0) trained with Kev's
code. JuL follows TypeSafe's public System One API and uses no TypeSafe or Jev code, weights or
outputs.