mihailgribov/typecastlm-qwen3.5-3.8b
typecastlm-qwen3.5-3.8b
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:
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.713. 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, so a probability means what it says.
- 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.
Start
pip install "typecastlm[local]" flash-linear-attention fla-corefrom 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 answersreader.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 29. 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 and calibrated for a threshold at 0.5. 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. No criterion is written for unsure: it carries what neither of the two fits. The two modes are fitted separately, so a number from one is not a number from the other — read whichever mode you act on, and do not divide the logits of one by the temperature of the other. 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:
On FEVER the third answer separates NOT ENOUGH INFO from decidable rows with AUC 0.713, and three-class accuracy is 0.668.
Speed, one decision at a time on a 16 GB consumer card. What sets it is the length of the material, not the question:
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:
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.
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.
