TypeCastLM-Qwen3.5-3.8B
Version 1.4.0 · what changed: Versions
A Jev-class decision model with open weights. You hand it material and a question; it answers with numbers, not prose. Four modes, one forward pass each:
| mode | the question | the answer |
|---|---|---|
noul | yes or no, two criteria | p(yes), a softmax over those two answers |
tfu | the same question | one distribution over true, false, unsure |
choice | 2–26 options, one correct | a probability per option |
scale | an ordinal rubric, 10 levels or fewer | a probability per level |
noul, choice and scale are Jev's modes under Jev's names; tfu is the one Jev does not
have.
The third answer is what a two-answer reader cannot give. unsure is an output of its own,
not a hedged yes: no criterion asks for it, and it carries what neither of the two criteria fits.
On FEVER it separates undecidable material from decidable with AUC 0.771. Threshold it to abstain,
to route to a human, or to drop a document from a pipeline. It is read in its own mode, with its
own temperature, and stays out of noul, whose probability is a softmax over the two answers
that decide the question and nothing else.
Why this one:
- Fast — p50 48 ms on material under 200 tokens and 566 ms at 1000–4000, on a 16 GB consumer card; nothing is generated, so a decision is one forward pass and no tokens written.
- Calibrated — a temperature per mode ships with the weights:
tfu,choiceandscaleare calibrated,noulis set to answer decisively. - Small and open — 3.76B parameters, 7.5 GB of weights, Apache-2.0, one card.
- Four modes over one head, each with its own calibration.
| client and server | typecastlm on PyPI — pip install typecastlm |
| source | github.com/mihail-gribov/typecastlm |
| HTTP contract | docs/API.md — the Jev API, served from these weights |
On JevBench
The official JevBench open-weights board (v1.6.1, measured 7 October 2026, benchmarkheaven.com/jev-models), decoder systems that read a frozen base model. Place is among the 128 open-weights systems by Composite Score (accuracy, calibration, speed and cost); Capability Score is accuracy and calibration alone.
| place | system | base | Composite Score | Capability Score |
|---|---|---|---|---|
| 2 | decisio v0.8.0 | Gemma-4-31B | 71.7 | 79.6 |
| 8 | deck-31B | Gemma-4-31B | 68.7 | 77.6 |
| 9 | Cygnet | Gemma-4-12B | 68.6 | 70.9 |
| 11 | decisio v0.9.0 | Gemma-4-12B | 67.8 | 70.0 |
| 15 | decider chat | Gemma-4-31B | 66.1 | 70.6 |
| 32 | jev-local | Qwen3.5-9B | 41.3 | 57.5 |
| 45 | TypeCastLM 1.4.0 | Qwen3.5-4B | 29.8 | 56.4 |
| 49 | jqv | Qwen3-32B | 25.4 | 61.9 |
| 74 | SemIf | Qwen3.5-4B | 15.5 | 55.4 |
| 77 | local-jev | Qwen3.5-4B | 11.4 | 55.0 |
| 82 | open-alternative-jev | Qwen3.5-4B | 9.4 | 49.1 |
| 85 | Nemotron Diffusion 8B | Nemotron-8B | 7.3 | 47.5 |
Start
pip install "typecastlm[local]" flash-linear-attention fla-core
from typecastlm import Reader
r = Reader("mihailgribov/typecastlm-qwen3.5-3.8b")
a = r.noul(document, "Is the claim supported by the material?",
true="the material supports it", false="the material contradicts it")
a["p"]["yes"] # 0.83 — over yes and no
t = r.tfu(document, "Is the claim supported by the material?",
true="the material supports it", false="the material contradicts it")
t["p"]["unsure"] # 0.12 — the same reading, over three answers
reader.py in this repository is the same code, if you would rather not install the package. As
an HTTP service: pip install "typecastlm[server]" and typecastlm-serve --model mihailgribov/typecastlm-qwen3.5-3.8b, which answers the Jev API — same route, same request body,
same answer objects — so a client written against that interface reaches it by changing the base
URL.
Or without any of that — it is an ordinary classifier:
from transformers import pipeline
pipe = pipeline("text-classification", model="mihailgribov/typecastlm-qwen3.5-3.8b",
top_k=None, function_to_apply="none")
The head has 39 outputs: true, false, unsure, and one per answer mark (mark_A … mark_Z,
mark_0 … mark_9). They are answers to different questions, so take the ones your question uses
and softmax over those — never over all 39. The wording the numbers below were measured with is in
prompt.json.
Modes
Every mode takes the material and a question and differs in what the answer ranges over. One
forward pass each; each has its own temperature in prompt.json.
noul: yes or no
r.noul(policy, "Is the claim covered?",
true="the policy covers it", false="the policy excludes it")
# {"p": {"yes": 0.83, "no": 0.17}, "logits": {...}}
Two criteria, one per side, and a softmax over the two answers they name, at the verdict
temperature. That temperature is sharper than a calibrated one, so a clear case reads as a clear
yes or no rather than a cautious 0.7; refit it (below) if you need probabilities to match
frequencies. The third output takes no part in it. Outputs read: true, false.
tfu: yes, no, or neither
r.tfu(policy, "Is the claim covered?",
true="the policy covers it", false="the policy excludes it")
# {"p": {"true": 0.62, "false": 0.22, "unsure": 0.16}, "verdict": "true", "logits": {...}}
The same two criteria and the same forward pass, softmaxed over three answers at the
three_answers temperature, which is calibrated. No criterion is written for unsure: it carries
what neither of the two fits. Between true and false the two modes always agree; noul only
states it more decisively. Outputs read: true, false, unsure.
choice: one of several options
r.choice(policy, "How should the claim be settled?",
[("deny", "excluded as repeated seepage"),
("sublimit", "covered but capped by the concealed-water sublimit"),
("pay", "covered in full")])
# {"p": {"deny": 0.01, "sublimit": 0.97, "pay": 0.02}, "marks": {...}, "logits": {...}}
Two to twenty-six options, exactly one correct, no order among them. Option names are yours and
do not reach the prompt: inside, options are marked A to Z and marks says which got which.
Reading holds to about six options and then slips: 0.995 up to six, 0.965 at eight, 0.935 at
twelve, 0.86 from sixteen on, measured on a synthetic task with one right answer among K.
A mark row is the model's own output row for that token, so the head is a cache of rows that are
already in the weights: reader.py takes one from the embedding if the head lacks it, to the same
number. The head carries all 26 letters, so the plain text-classification pipeline reaches 26
too.
scale: a level on an ordinal rubric
r.scale(review, "How positive is this review?",
[("0", "very negative"), ("1", "negative"), ("2", "neutral"),
("3", "positive"), ("4", "very positive")])
# {"p": {"0": 0.06, "1": 0.08, "2": 0.11, "3": 0.54, "4": 0.20}, ...}
Levels are ordered and marked with their own digits when those are single characters, otherwise
with 0…9ABC…. Ten levels is the recommendation; up to 36 are accepted, but past nine the reading
degrades — 0.05 accuracy on levels 10–14 against 0.215 on 0–9. Digits beat letters where both fit:
on SST-5, 0.460 against 0.410. Outputs read: mark_0 … one per level.
Several questions about one state
r.noul_many(policy, [(q1, true1, false1), (q2, true2, false2)])
Returns one noul result per question. The state is read again for each, so a bundle costs what the
questions cost separately.
Numbers
Public pools, validation splits:
| benchmark | rows | AUC | accuracy @0.5 | calibration error |
|---|---|---|---|---|
| SuperGLUE BoolQ | 3000 | 0.942 | 0.872 | 0.087 |
| SuperGLUE RTE | 277 | 0.947 | 0.877 | 0.066 |
| FEVER (NLI form), decidable rows | 1395 | 0.953 | 0.898 | 0.061 |
Calibration error is at the shipped, deliberately sharp noul temperature. On FEVER the third
answer separates NOT ENOUGH INFO from decidable rows with AUC 0.771, and three-class accuracy is
0.698 (2089 rows).
Speed, one decision at a time on a 16 GB consumer card. What sets it is the length of the material, not the question:
| material | p50 | p90 |
|---|---|---|
| under 200 tokens | 48 ms | 75 ms |
| 200–1000 | 93 ms | 140 ms |
| 1000–4000 | 566 ms | 666 ms |
Install flash-linear-attention and fla-core, or the trunk falls back to a slow path and p50
triples.
Calibration
The values are in prompt.json under calibration, one temperature per mode, applied by
reader.py and by the client. What each was fitted on, since that is what decides whether it
carries over to your material:
| mode | key in prompt.json | fitted on |
|---|---|---|
noul | verdict | a broad set of yes/no decisions — rules, evidence, requests and answers; set sharper than calibrated, so that confident answers read as confident |
tfu | three_answers | the same material with yes/no/unknown answers |
choice | choice | a broad set of multiple-option decisions |
scale | scale | ordinal rubrics over the same kinds of material |
A temperature changes no answer, only the probability. If your pool is harder or easier than
those, refit on a couple of hundred labelled rows with typecastlm.calibrate, or read the logits
and skip it:
from typecastlm import calibrate
rows = [(r.noul(text, q, true=T, false=F)["logits"], gold) for text, q, gold in my_labelled]
calibrate(rows)["temperature"] # gold is "true" or "false"
Fit one per mode, on rows read in that mode: a temperature corrects how confident the reader sounds against how hard your material is, so it does not carry from one mode to another any more than it carries from an easy pool to a hard one.
Versions
1.4.0 puts separation first: the head tells right from wrong answers better than 1.3.0 did —
AUC 0.920 → 0.942 on BoolQ, 0.931 → 0.947 on RTE, 0.950 → 0.953 on FEVER — and noul answers more
decisively. The head and its temperatures were fitted on a pool about three times larger and broader
than the one behind 1.3.0. Versions are tagged in this repository (v1.2.2, v1.3.0, v1.4.0); main is the
latest, and from_pretrained(…, revision="v1.3.0") keeps an earlier one.
Licence
Apache-2.0 — the weights, inherited from
Qwen3.5-4B, and the texts here. LICENSE carries the
licence, NOTICE states what was changed.
Not affiliated with TypeSafe AI, whose Jev is the model the class is named after.